stream-compare
Advanced tools
Comparing version 0.1.0 to 0.2.0
# Change Log | ||
## [0.1.0](https://github.com/kevinoid/stream-compare/tree/0.1.0) (2016-04-02) | ||
## [v0.2.0](https://github.com/kevinoid/stream-compare/tree/v0.2.0) (2016-04-22) | ||
### Breaking Changes | ||
- Remove callback argument and return Promise unconditionally. Consider using | ||
[promise-nodeify](https://github.com/kevinoid/promise-nodeify) to migrate. | ||
### New Features | ||
- `endEvents` option for controlling comparison on stream end. | ||
- `.checkpoint()` method on returned Promise allows caller to trigger compare. | ||
- `.end()` method on returned Promise allows caller to end comparison. | ||
## [v0.1.0](https://github.com/kevinoid/stream-compare/tree/v0.1.0) (2016-04-02) | ||
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* |
504
index.js
@@ -8,5 +8,19 @@ /** | ||
var EventEmitter = require('events').EventEmitter; | ||
var Promise = require('any-promise'); | ||
var debug = require('debug')('stream-compare'); | ||
var extend = require('extend'); | ||
/** Comparison type. | ||
* @enum {string} | ||
* @private | ||
*/ | ||
var CompareType = { | ||
/** A full (non-incremental) comparison. */ | ||
checkpoint: 'checkpoint', | ||
/** An incremental comparison. */ | ||
incremental: 'incremental', | ||
/** A full comparison followed by <code>'end'</code>. */ | ||
last: 'last' | ||
}; | ||
/** Defines the available read policies. | ||
@@ -16,4 +30,13 @@ * @enum {string} | ||
var ReadPolicy = { | ||
/** Reads are done concurrently using <code>'data'</code> events. */ | ||
flowing: 'flowing', | ||
/** Reads from the stream which has output the least data, measured in | ||
* bytes/chars for non-<code>objectMode</code> or values for | ||
* <code>objectMode</code>. */ | ||
least: 'least', | ||
/** No reads are done. When using this readPolicy, be sure to either add | ||
* <code>'data'</code> to events, add other <code>'data'</code> listeners, | ||
* <code>.read()</code> the data elsewhere, or call <code>.resume()</code> on | ||
* the streams so that the data will be read and <code>'end'</code> can be | ||
* reached. */ | ||
none: 'none' | ||
@@ -24,2 +47,3 @@ }; | ||
* @const | ||
* @private | ||
*/ | ||
@@ -29,7 +53,7 @@ var DEFAULT_OPTIONS = { | ||
delay: 0, | ||
endEvents: ['end', 'error'], | ||
// Observe Readable events other than 'data' by default | ||
events: ['close', 'end', 'error'], | ||
objectMode: false, | ||
// Can be ''flowing', least', or 'none' | ||
// @type {!ReadPolicy} | ||
/** @type {!ReadPolicy} */ | ||
readPolicy: 'least' | ||
@@ -41,11 +65,14 @@ }; | ||
* Guarantees/Invariants: | ||
* - Equivalent states are assert.deepStrictEqual. | ||
* - States can be round-tripped to JSON at any point. | ||
* - States are owned by the caller, so any additional properties (which are | ||
* permitted to violate the above guarantees) are preserved and the same | ||
* state object is always returned. | ||
* | ||
* As a result, objects of this class have no methods and do not contain any | ||
* <ul> | ||
* <li>Equivalent states are {@link assert.deepStrictEqual}.</li> | ||
* <li>States can be round-tripped to JSON at any point.</li> | ||
* <li>States are owned by the caller, so any additional properties (which are | ||
* permitted to violate the above guarantees) are preserved and the same | ||
* state object is always returned.</li> | ||
* </ul> | ||
* | ||
* <p>As a result, objects of this class have no methods and do not contain any | ||
* non-state information (e.g. the stream itself or the comparison options) | ||
* and their prototype is never used. | ||
* and their prototype is never used.</p> | ||
* | ||
@@ -55,3 +82,3 @@ * @constructor | ||
function StreamState() { | ||
/** Has the stream emitted 'end' or 'error'. */ | ||
/** Has the stream emitted <code>'end'</code> or <code>'error'</code>. */ | ||
this.ended = false; | ||
@@ -61,13 +88,143 @@ /** Events emitted by the stream. | ||
this.events = []; | ||
/** Data returned/emitted by the stream (as an Array if in objectMode). | ||
/** Data returned/emitted by the stream (as an <code>Array</code> if in | ||
* <code>objectMode</code>). | ||
* @type Array|Buffer|string */ | ||
this.data = undefined; | ||
/** Count of total objects read in objectMode, bytes/chars read otherwise. */ | ||
/** Count of total objects read in <code>objectMode</code>, bytes/chars read | ||
* otherwise. */ | ||
this.totalDataLen = 0; | ||
} | ||
/** Same as streamCompare, but assumes its arguments are valid. | ||
* @private | ||
/** Options for {@link streamCompare}. | ||
* | ||
* @ template CompareResult | ||
* @typedef {{ | ||
* abortOnError: boolean|undefined, | ||
* compare: ((function(!StreamState,!StreamState): CompareResult)|undefined), | ||
* delay: number|undefined, | ||
* endEvents: Array<string>|undefined, | ||
* events: Array<string>|undefined, | ||
* incremental: | ||
* ((function(!StreamState,!StreamState): CompareResult)|undefined), | ||
* objectMode: boolean|undefined, | ||
* readPolicy: ReadPolicy|undefined | ||
* }} StreamCompareOptions | ||
* @property {boolean=} abortOnError Abort comparison and return error emitted | ||
* by either stream. (default: <code>false</code>) | ||
* @property {function(!StreamState,!StreamState)=} compare Comparison function | ||
* which will be called with a StreamState object for each stream, after both | ||
* streams have ended. The value returned by this function will resolve the | ||
* returned promise and be passed to the callback as its second argument. A | ||
* value thrown by this function will reject the promise and be passed to the | ||
* callback as its first argument. This function is required if incremental is | ||
* not specified. | ||
* @property {number=} delay Additional delay (in ms) after streams end before | ||
* comparing. (default: <code>0</code>) | ||
* @property {Array<string>=} endEvents Names of events which signal the end of | ||
* a stream. Final compare is performed once both streams have emitted an end | ||
* event. (default: <code>['end', 'error']</code>) | ||
* @property {Array<string>=} events Names of events to compare. | ||
* (default: <code>['close', 'end', 'error']</code>) | ||
* @property {function(!StreamState,!StreamState)=} incremental Incremental | ||
* comparison function which will be called periodically with a StreamState | ||
* object for each stream. This function may modify the StreamState objects to | ||
* remove data not required for later comparisons (e.g. common output) and may | ||
* perform the comparison before the streams have ended (e.g. due to early | ||
* differences). Any non-null, non-undefined value returned by this function | ||
* will finish the comparison, resolve the returned promise, and be passed to | ||
* the callback as its second argument. A value thrown by this function will | ||
* finish the comparison, reject the promise and be passed to the callback as | ||
* its first argument. If compare is not specified, this function will also be | ||
* called for the final comparison. | ||
* @property {boolean=} objectMode Collect values read into an Array. This | ||
* allows comparison of read values without concatenation and comparison of | ||
* non-string/Buffer types. | ||
* @property {ReadPolicy=} readPolicy Scheduling discipline for reads from th | ||
* streams. (default: <code>'least'</code>) | ||
*/ | ||
function streamCompareInternal(stream1, stream2, options, callback) { | ||
// var StreamCompareOptions; | ||
/** Promise returned by {@link streamCompare}. | ||
* | ||
* @ template CompareResult | ||
* @constructor | ||
* @name StreamComparePromise | ||
* @extends Promise<CompareResult> | ||
*/ | ||
// var StreamComparePromise; | ||
/** Compares the output of two Readable streams. | ||
* | ||
* @ template CompareResult | ||
* @param {!stream.Readable} stream1 First stream to compare. | ||
* @param {!stream.Readable} stream2 Second stream to compare. | ||
* @param {!StreamCompareOptions<CompareResult>| | ||
* function(!StreamState,!StreamState): CompareResult} | ||
* optionsOrCompare Options, or a comparison function (as described in | ||
* {@link options.compare}). | ||
* @return {StreamComparePromise<CompareResult>} A <code>Promise</code> with | ||
* the comparison result or error. | ||
*/ | ||
function streamCompare(stream1, stream2, optionsOrCompare) { | ||
var options; | ||
if (optionsOrCompare) { | ||
if (typeof optionsOrCompare === 'function') { | ||
options = {compare: optionsOrCompare}; | ||
} else if (typeof optionsOrCompare === 'object') { | ||
options = optionsOrCompare; | ||
} else { | ||
throw new TypeError('optionsOrCompare must be an object or function'); | ||
} | ||
} | ||
options = extend({}, DEFAULT_OPTIONS, options); | ||
if (!options.compare) { | ||
options.compare = options.incremental; | ||
} | ||
// Can change this to duck typing if there are non-EventEmitter streams | ||
if (!(stream1 instanceof EventEmitter)) { | ||
throw new TypeError('stream1 must be an EventEmitter'); | ||
} | ||
// Can change this to duck typing if there are non-EventEmitter streams | ||
if (!(stream2 instanceof EventEmitter)) { | ||
throw new TypeError('stream2 must be an EventEmitter'); | ||
} | ||
if (options.readPolicy === 'least' && | ||
(typeof stream1.read !== 'function' || | ||
typeof stream2.read !== 'function')) { | ||
throw new TypeError('streams must have .read() for readPolicy \'least\''); | ||
} | ||
if (typeof options.compare !== 'function') { | ||
throw new TypeError('options.compare must be a function'); | ||
} | ||
if (!options.endEvents || | ||
typeof options.endEvents !== 'object' || | ||
options.endEvents.length !== options.endEvents.length | 0) { | ||
throw new TypeError('options.endEvents must be Array-like'); | ||
} | ||
options.endEvents = Array.prototype.slice.call(options.endEvents); | ||
if (!options.events || | ||
typeof options.events !== 'object' || | ||
options.events.length !== options.events.length | 0) { | ||
throw new TypeError('options.events must be Array-like'); | ||
} | ||
options.events = Array.prototype.slice.call(options.events); | ||
if (options.incremental && typeof options.incremental !== 'function') { | ||
throw new TypeError('options.incremental must be a function'); | ||
} | ||
if (typeof options.readPolicy !== 'string') { | ||
throw new TypeError('options.readPolicy must be a string'); | ||
} | ||
if (!ReadPolicy.hasOwnProperty(options.readPolicy)) { | ||
throw new RangeError('Invalid options.readPolicy \'' + | ||
options.readPolicy + '\''); | ||
} | ||
var reject; | ||
var resolve; | ||
var promise = new Promise(function(resolveArg, rejectArg) { | ||
resolve = resolveArg; | ||
reject = rejectArg; | ||
}); | ||
var state1 = new StreamState(); | ||
@@ -79,11 +236,15 @@ var state2 = new StreamState(); | ||
var listeners2 = {}; | ||
var endListener1; | ||
var endListener2; | ||
var postEndImmediate; | ||
var postEndTimeout; | ||
/** Gets the name of a stream for logging purposes. */ | ||
/** Gets the name of a stream for logging purposes. | ||
* @private | ||
*/ | ||
function streamName(stream) { | ||
return stream === stream1 ? 'stream1' : 'stream2'; | ||
return stream === stream1 ? 'stream1' : | ||
stream === stream2 ? 'stream2' : | ||
'unknown stream'; | ||
} | ||
function done() { | ||
function done(err, result) { | ||
isDone = true; | ||
@@ -97,6 +258,7 @@ | ||
stream1.removeListener('readable', readNext); | ||
stream1.removeListener('error', endListener1); | ||
stream1.removeListener('error', done); | ||
stream1.removeListener('error', onStreamError); | ||
stream1.removeListener('end', readNextOnEnd); | ||
stream1.removeListener('end', endListener1); | ||
options.endEvents.forEach(function(eventName) { | ||
stream1.removeListener(eventName, endListener1); | ||
}); | ||
@@ -107,36 +269,44 @@ Object.keys(listeners2).forEach(function(eventName) { | ||
stream2.removeListener('readable', readNext); | ||
stream2.removeListener('error', endListener2); | ||
stream2.removeListener('error', done); | ||
stream2.removeListener('error', onStreamError); | ||
stream2.removeListener('end', readNextOnEnd); | ||
stream2.removeListener('end', endListener2); | ||
options.endEvents.forEach(function(eventName) { | ||
stream2.removeListener(eventName, endListener2); | ||
}); | ||
debug('All done. Calling callback...'); | ||
return callback.apply(this, arguments); | ||
clearImmediate(postEndImmediate); | ||
clearTimeout(postEndTimeout); | ||
debug('Comparison finished.'); | ||
} | ||
function doCompare() { | ||
debug('Performing final compare.'); | ||
var result, resultErr; | ||
try { | ||
result = options.compare(state1, state2); | ||
} catch (err) { | ||
resultErr = err; | ||
} | ||
done(resultErr, result); | ||
function onStreamError(err) { | ||
debug(streamName(this) + ' emitted error', err); | ||
reject(err); | ||
done(); | ||
} | ||
function doneIfIncremental() { | ||
var result, resultErr, threw; | ||
function doCompare(compareFn, type) { | ||
debug('Performing %s compare.', type); | ||
var hasResultOrError = false; | ||
try { | ||
result = options.incremental(state1, state2); | ||
var result = compareFn(state1, state2); | ||
if (result !== undefined && result !== null) { | ||
debug('Comparison produced a result:', result); | ||
hasResultOrError = true; | ||
resolve(result); | ||
} | ||
} catch (err) { | ||
threw = true; | ||
resultErr = err; | ||
debug('Comparison produced an error:', err); | ||
hasResultOrError = true; | ||
reject(err); | ||
} | ||
if ((result !== undefined && result !== null) || threw) { | ||
debug('Incremental comparison was conclusive. Finishing...'); | ||
done(resultErr, result); | ||
if (hasResultOrError) { | ||
done(); | ||
return true; | ||
} else if (type === CompareType.last) { | ||
resolve(); | ||
done(); | ||
return true; | ||
} | ||
@@ -147,4 +317,31 @@ | ||
/** Compares the states of the two streams non-incrementally. | ||
* @function | ||
* @name StreamComparePromise#checkpoint | ||
*/ | ||
promise.checkpoint = function checkpoint() { | ||
if (isDone) { | ||
debug('Ignoring checkpoint() after settling.'); | ||
return; | ||
} | ||
doCompare(options.compare, CompareType.checkpoint); | ||
}; | ||
/** Compares the states of the two streams non-incrementally then ends the | ||
* comparison whether or not compare produced a result or error. | ||
* @function | ||
* @name StreamComparePromise#end | ||
*/ | ||
promise.end = function end() { | ||
if (isDone) { | ||
debug('Ignoring end() after settling.'); | ||
return; | ||
} | ||
doCompare(options.compare, CompareType.last); | ||
}; | ||
// Note: Add event listeners before endListeners so end/error is recorded | ||
Array.prototype.forEach.call(options.events, function(eventName) { | ||
options.events.forEach(function(eventName) { | ||
if (listeners1[eventName]) { | ||
@@ -159,5 +356,3 @@ return; | ||
function listener() { | ||
debug('\'' + eventName + '\' event from ' + streamName(this) + '.'); | ||
function listener(/* event args */) { | ||
this.events.push({ | ||
@@ -169,23 +364,32 @@ name: eventName, | ||
if (options.incremental) { | ||
doneIfIncremental(); | ||
doCompare(options.incremental, CompareType.incremental); | ||
} | ||
} | ||
listeners1[eventName] = listener.bind(state1); | ||
listeners1[eventName] = function listener1() { | ||
debug('\'' + eventName + '\' event from stream1.'); | ||
listener.apply(state1, arguments); | ||
}; | ||
stream1.on(eventName, listeners1[eventName]); | ||
listeners2[eventName] = listener.bind(state2); | ||
listeners2[eventName] = function listener2() { | ||
debug('\'' + eventName + '\' event from stream2.'); | ||
listener.apply(state2, arguments); | ||
}; | ||
stream2.on(eventName, listeners2[eventName]); | ||
}); | ||
/** @this {!StreamState} */ | ||
function endListener() { | ||
/** Handles stream end events. | ||
* @this {!Readable} | ||
* @private | ||
*/ | ||
function endListener(state) { | ||
// Note: If incremental is conclusive for 'end' event, this will be called | ||
// with isDone === true, since removeListener doesn't affect listeners for | ||
// an event which is already in-progress. | ||
if (this.ended || isDone) { | ||
if (state.ended || isDone) { | ||
return; | ||
} | ||
this.ended = true; | ||
state.ended = true; | ||
++ended; | ||
@@ -196,3 +400,3 @@ | ||
if (options.incremental) { | ||
if (doneIfIncremental()) { | ||
if (doCompare(options.incremental, CompareType.incremental)) { | ||
return; | ||
@@ -203,10 +407,13 @@ } | ||
if (ended === 2) { | ||
var postEndCompare = function() { | ||
doCompare(options.compare, CompareType.last); | ||
}; | ||
if (options.delay) { | ||
debug('All streams have ended. Delaying for ' + options.delay + | ||
'ms before final compare.'); | ||
setTimeout(doCompare, options.delay); | ||
postEndTimeout = setTimeout(postEndCompare, options.delay); | ||
} else { | ||
// Let pending I/O and callbacks complete to catch some errant events | ||
debug('All streams have ended. Delaying before final compare.'); | ||
setImmediate(doCompare); | ||
postEndImmediate = setImmediate(postEndCompare); | ||
} | ||
@@ -216,14 +423,18 @@ } | ||
endListener1 = endListener.bind(state1); | ||
stream1.on('end', endListener1); | ||
function endListener1() { | ||
endListener.call(this, state1); | ||
} | ||
function endListener2() { | ||
endListener.call(this, state2); | ||
} | ||
options.endEvents.forEach(function(eventName) { | ||
if (!options.abortOnError || eventName !== 'error') { | ||
stream1.on(eventName, endListener1); | ||
stream2.on(eventName, endListener2); | ||
} | ||
}); | ||
endListener2 = endListener.bind(state2); | ||
stream2.on('end', endListener2); | ||
if (options.abortOnError) { | ||
stream1.once('error', done); | ||
stream2.once('error', done); | ||
} else { | ||
stream1.on('error', endListener1); | ||
stream2.on('error', endListener2); | ||
stream1.once('error', onStreamError); | ||
stream2.once('error', onStreamError); | ||
} | ||
@@ -239,2 +450,3 @@ | ||
* @param {*} data Data read from the stream for this StreamState. | ||
* @private | ||
*/ | ||
@@ -284,8 +496,14 @@ function addData(data) { | ||
/** Handles data read from the stream for a given state. */ | ||
/** Handles data read from the stream for a given state. | ||
* @private | ||
*/ | ||
function handleData(state, data) { | ||
debug('Read data from ', streamName(this)); | ||
try { | ||
addData.call(state, data); | ||
} catch (err) { | ||
done(err); | ||
debug('Error adding data from ' + streamName(this), err); | ||
reject(err); | ||
done(); | ||
return; | ||
@@ -295,7 +513,9 @@ } | ||
if (options.incremental) { | ||
doneIfIncremental(); | ||
doCompare(options.incremental, CompareType.incremental); | ||
} | ||
} | ||
/** Reads from the non-ended stream which has the smallest totalDataLen. */ | ||
/** Reads from the non-ended stream which has the smallest totalDataLen. | ||
* @private | ||
*/ | ||
function readNext() { | ||
@@ -324,3 +544,3 @@ var stream, state; | ||
handleData(state, data); | ||
handleData.call(stream, state, data); | ||
} | ||
@@ -334,2 +554,4 @@ } | ||
* from the other stream. | ||
* | ||
* @private | ||
*/ | ||
@@ -351,4 +573,4 @@ function readNextOnEnd() { | ||
debug('Will read from streams in flowing mode.'); | ||
stream1.on('data', handleData.bind(null, state1)); | ||
stream2.on('data', handleData.bind(null, state2)); | ||
stream1.on('data', handleData.bind(stream1, state1)); | ||
stream2.on('data', handleData.bind(stream2, state2)); | ||
break; | ||
@@ -360,3 +582,3 @@ | ||
stream2.once('end', readNextOnEnd); | ||
readNext(); | ||
process.nextTick(readNext); | ||
break; | ||
@@ -368,128 +590,4 @@ | ||
} | ||
} | ||
/** Compares the output of two Readable streams. | ||
* | ||
* Options: | ||
* - abortOnError: Abort comparison and return error emitted by either stream. | ||
* (default: false) | ||
* - compare: Comparison function which will be called with a StreamState | ||
* object for each stream, after both streams have ended. The value | ||
* returned by this function will resolve the returned promise and be passed | ||
* to the callback as its second argument. A value thrown by this function | ||
* will reject the promise and be passed to the callback as its first | ||
* argument. | ||
* This function is required if incremental is not specified. | ||
* - delay: Additional delay (in ms) after streams end before comparing. | ||
* (default: 0) | ||
* - events: Names of events to compare. (default: ['close', 'end', 'error']) | ||
* - incremental: Incremental comparison function which will be called | ||
* periodically with a StreamState object for each stream. This function | ||
* may modify the StreamState objects to remove data not required for later | ||
* comparisons (e.g. common output) and may perform the comparison before | ||
* the streams have ended (e.g. due to early differences). Any non-null, | ||
* non-undefined value returned by this function will finish the comparison, | ||
* resolve the returned promise, and be passed to the callback as its second | ||
* argument. A value thrown by this function will finish the comparison, | ||
* reject the promise and be passed to the callback as its first argument. | ||
* If compare is not specified, this function will also be called for the | ||
* final comparison. | ||
* - objectMode: Collect values read into an Array. This allows comparison | ||
* of read values without concatenation and comparison of non-string/Buffer | ||
* types. | ||
* - readPolicy: Scheduling discipline for reads from the streams. | ||
* - 'flowing': Reads are done concurrently using 'data' events. | ||
* - 'least': Reads from the stream which has output the least data, measured | ||
* in bytes/chars for non-objectMode or values for objectMode. | ||
* - 'none': No reads are done. When using this readPolicy, be sure to | ||
* either add 'data' to events, add other 'data' listeners, .read() the | ||
* data elsewhere, or call .resume() on the streams so that the data will | ||
* be read and 'end' can be reached. | ||
* (default: 'least') | ||
* | ||
* @param {!stream.Readable} stream1 First stream to compare. | ||
* @param {!stream.Readable} stream2 Second stream to compare. | ||
* @param {!Object|function(!StreamState,!StreamState)} optionsOrCompare | ||
* Options, or a comparison function (as described in options.compare). | ||
* @param {?function(Error, Object=)=} callback Callback with comparison result | ||
* or error. | ||
* @return {Promise|undefined} If <code>callback</code> is not given and | ||
* <code>global.Promise</code> is defined, a <code>Promise</code> with the | ||
* comparison result or error. | ||
*/ | ||
function streamCompare(stream1, stream2, optionsOrCompare, callback) { | ||
if (!callback && typeof Promise === 'function') { | ||
// eslint-disable-next-line no-undef | ||
return new Promise(function(resolve, reject) { | ||
streamCompare(stream1, stream2, optionsOrCompare, function(err, result) { | ||
if (err) { reject(err); } else { resolve(result); } | ||
}); | ||
}); | ||
} | ||
if (typeof callback !== 'function') { | ||
throw new TypeError('callback must be a function'); | ||
} | ||
// From this point on errors are returned using callback. As long as callers | ||
// pass a callback or have global.Promise, this function will never throw and | ||
// callers don't need to wrap in a try/catch. | ||
var options; | ||
try { | ||
if (optionsOrCompare) { | ||
if (typeof optionsOrCompare === 'function') { | ||
options = {compare: optionsOrCompare}; | ||
} else if (typeof optionsOrCompare === 'object') { | ||
options = optionsOrCompare; | ||
} else { | ||
throw new TypeError('optionsOrCompare must be an object or function'); | ||
} | ||
} | ||
options = extend({}, DEFAULT_OPTIONS, options); | ||
if (!options.compare) { | ||
options.compare = options.incremental; | ||
} | ||
// Can change this to duck typing if there are non-EventEmitter streams | ||
if (!(stream1 instanceof EventEmitter)) { | ||
throw new TypeError('stream1 must be an EventEmitter'); | ||
} | ||
// Can change this to duck typing if there are non-EventEmitter streams | ||
if (!(stream2 instanceof EventEmitter)) { | ||
throw new TypeError('stream2 must be an EventEmitter'); | ||
} | ||
if (options.readPolicy === 'least' && | ||
(typeof stream1.read !== 'function' || | ||
typeof stream2.read !== 'function')) { | ||
throw new TypeError('streams must have .read() for readPolicy \'least\''); | ||
} | ||
if (typeof options.compare !== 'function') { | ||
throw new TypeError('options.compare must be a function'); | ||
} | ||
if (!options.events || | ||
typeof options.events !== 'object' || | ||
options.events.length !== options.events.length | 0) { | ||
throw new TypeError('options.events must be Array-like'); | ||
} | ||
if (options.incremental && typeof options.incremental !== 'function') { | ||
throw new TypeError('options.incremental must be a function'); | ||
} | ||
if (typeof options.readPolicy !== 'string') { | ||
throw new TypeError('options.readPolicy must be a string'); | ||
} | ||
if (!ReadPolicy.hasOwnProperty(options.readPolicy)) { | ||
throw new RangeError('Invalid options.readPolicy \'' + | ||
options.readPolicy + '\''); | ||
} | ||
} catch (err) { | ||
process.nextTick(function() { | ||
callback(err); | ||
}); | ||
return undefined; | ||
} | ||
streamCompareInternal(stream1, stream2, options, callback); | ||
return undefined; | ||
return promise; | ||
} | ||
@@ -496,0 +594,0 @@ |
@@ -8,3 +8,14 @@ /** | ||
/** Incrementally compares and reduces an Array-like property of the states | ||
* using a given comparison function. */ | ||
* using a given comparison function. | ||
* | ||
* @ template CompareResult | ||
* @param {!StreamState} state1 First state to compare. | ||
* @param {!StreamState} state2 Second state to compare. | ||
* @param {string} propName Name of Array-like property to compare. | ||
* @param {function(*, *): CompareResult} compare Comparison function to apply | ||
* to the <code>propName</code> values from each state. | ||
* @return {CompareResult} Result of comparing <code>propName</code> values, up | ||
* to the minimum common length. | ||
* @private | ||
*/ | ||
function incrementalProp(state1, state2, propName, compare) { | ||
@@ -43,18 +54,22 @@ var values1 = state1[propName]; | ||
* | ||
* Given a function which compares output data (e.g. assert.deepStrictEqual), | ||
* this function returns an incremental comparison function which compares | ||
* only the amount of data output by both streams (unless the stream has ended, | ||
* in which case all remaining data is compared) and removes the compared data | ||
* if no comparison result is returned/thrown. | ||
* Given a function which compares output data (e.g. | ||
* <code>assert.deepStrictEqual</code>), this function returns an incremental | ||
* comparison function which compares only the amount of data output by both | ||
* streams (unless the stream has ended, in which case all remaining data is | ||
* compared) and removes the compared data if no comparison result is | ||
* returned/thrown. | ||
* | ||
* @param {function((string|Buffer|Array), (string|Buffer|Array))} compareData | ||
* Data comparison function which will be called with data output by each | ||
* @ template CompareResult | ||
* @param {function((string|Buffer|Array), (string|Buffer|Array)): | ||
* CompareResult} compareData Data comparison function which will be called | ||
* with data output by each stream. | ||
* @param {?function(!Array<!{name:string,args:Array}>, | ||
* !Array<!{name:string,args:Array}>): CompareResult=} compareEvents Events | ||
* comparison function which will be called with the events output by each | ||
* stream. | ||
* @param {?function(!Array.<!{name:string,args:Array}>, | ||
* !Array.<!{name:string,args:Array}>)=} compareEvents Events comparison | ||
* function which will be called with be called with with events output by | ||
* each stream. | ||
* @returns {function(!StreamState, !StreamState)} Incremental comparison | ||
* function which compares the stream states using compareData and | ||
* compareEvents, and removes compared values if no result is reached. | ||
* @returns {function(!StreamState, !StreamState): CompareResult} Incremental | ||
* comparison function which compares the stream states using | ||
* <code>compareData</code> and <code>compareEvents</code>, and removes | ||
* compared values if no result is reached. | ||
* @alias makeIncremental | ||
*/ | ||
@@ -61,0 +76,0 @@ module.exports = function makeIncremental(compareData, compareEvents) { |
{ | ||
"name": "stream-compare", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Compare the behavior of readable streams.", | ||
@@ -32,3 +32,3 @@ "keywords": [ | ||
"postversion": "rimraf doc && git clone -b gh-pages -l -q . doc && npm run doc && git -C doc add . && git -C doc commit -n -m \"Docs for v$npm_package_version\"", | ||
"preversion": "git-branch-is master && travis-status -b -c -qwx && depcheck && david", | ||
"preversion": "git-branch-is master && travis-status -b -c -qwx && depcheck --ignores bluebird && david", | ||
"test": "npm run lint && npm run test-unit", | ||
@@ -43,2 +43,3 @@ "test-cov": "npm run lint && npm run test-unit-cov", | ||
"dependencies": { | ||
"any-promise": "^1.1.0", | ||
"debug": "^2.2.0", | ||
@@ -53,3 +54,4 @@ "extend": "^3.0.0" | ||
"eslint": "^2.3.0", | ||
"eslint-config-airbnb": "^6.0.1", | ||
"eslint-config-airbnb-base": "^1.0.3", | ||
"eslint-plugin-import": "^1.5.0", | ||
"istanbul": "^0.4.1", | ||
@@ -56,0 +58,0 @@ "jsdoc": "^3.4.0", |
@@ -22,3 +22,3 @@ stream-compare | ||
var stream2 = fs.createReadStream(file); | ||
streamCompare(stream1, stream2, assert.deepStrictEqual, function(err) { | ||
streamCompare(stream1, stream2, assert.deepStrictEqual).catch(function(err) { | ||
console.log(err); // AssertionError if streams differ | ||
@@ -36,3 +36,3 @@ }); | ||
- Support for caller-defined comparisons, which can return errors or values | ||
and are not limited to equality. | ||
not limited to equality. | ||
- Support for both incremental and one-shot comparisons. | ||
@@ -82,3 +82,3 @@ - Support for caller-defined data reduction to avoid storing the entire stream | ||
}; | ||
streamCompare(stream1, stream2, options, function(err) { | ||
streamCompare(stream1, stream2, options).catch(function(err) { | ||
console.log(err); // AssertionError if stream data values differ | ||
@@ -99,3 +99,3 @@ }); | ||
}; | ||
streamCompare(stream1, stream2, options, function(err) { | ||
streamCompare(stream1, stream2, options).catch(function(err) { | ||
console.log(err); // AssertionError if stream data values differ | ||
@@ -118,3 +118,3 @@ }); | ||
}; | ||
streamCompare(stream1, stream2, options, function(err) { | ||
streamCompare(stream1, stream2, options).catch(function(err) { | ||
console.log(err); // AssertionError if stream events (including 'data') differ | ||
@@ -124,2 +124,34 @@ }); | ||
### Control comparison checkpoints | ||
The returned Promise includes additional methods for controlling the | ||
comparison. A non-incremental compare can be run before both streams end | ||
using `.checkpoint()`. Additionally, the comparison can be concluded before | ||
both streams end using `.end()`. The full details are available in the [API | ||
Documentation](https://kevinoid.github.io/stream-compare/api/StreamComparePromise.html). | ||
```js | ||
var PassThrough = require('stream').PassThrough; | ||
var stream1 = new PassThrough(); | ||
var stream2 = new PassThrough(); | ||
var comparison = streamCompare(stream1, stream2, assert.deepStrictEqual); | ||
comparison.then( | ||
function() { console.log('streams are equal'); }, | ||
function(err) { console.log('streams differ: ' + err); } | ||
); | ||
stream1.write('Hello'); | ||
stream2.write('Hello'); | ||
process.nextTick(function() { | ||
comparison.checkpoint(); | ||
stream1.write(' world!'); | ||
stream2.write(' world!'); | ||
process.nextTick(function() { | ||
comparison.end(); | ||
}); | ||
}); | ||
``` | ||
More examples can be found in the [test | ||
@@ -126,0 +158,0 @@ specifications](https://kevinoid.github.io/stream-compare/specs). |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
98191
7
596
176
3
13
+ Addedany-promise@^1.1.0
+ Addedany-promise@1.3.0(transitive)