From 2045c76f7e40878f2de308fb2579899a86fea80f Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 26 Jan 2017 16:03:00 -0500 Subject: [PATCH] Integrate Store into DataValidator, ValidStateMonitor ValidStateMonitor now uses a Store in place of the original primitive object-based diff format. The original format is translated by DataValidator. The code is in a transitional state, and considering the amount of time we spend on various areas of this project, will likely stay this way for a while. * src/validate/DataValidator.js (__construct): Accept Store factory parameter. (_store_factory): Add field. (_createStores): Add method. (_validate): Handle Store. (updateFailures): Add method. (_populateStore): Add method. * test/validate/DataValidatorTest.js: Add tests. * src/validate/ValidStateMonitor.js (update): Enforce Store diff. Wait to process failures until fixes are calculated. (_checkFailureFix): Handle asynchronous, Promise-based diff. (_checkCauseFix): Extract logic from _checkCauseFix. * test/validate/ValidStateMonitorTest.js: Modify test cases to be Promise-based and handle async calls where appropriate. That was a friggin' expensive mess. DEV-2296 --- src/validate/DataValidator.js | 82 +++- src/validate/ValidStateMonitor.js | 150 ++++-- test/validate/DataValidatorTest.js | 53 ++- test/validate/ValidStateMonitorTest.js | 603 +++++++++++++++---------- 4 files changed, 576 insertions(+), 312 deletions(-) diff --git a/src/validate/DataValidator.js b/src/validate/DataValidator.js index 0fead8d..fa56ec6 100644 --- a/src/validate/DataValidator.js +++ b/src/validate/DataValidator.js @@ -55,6 +55,12 @@ module.exports = Class( 'DataValidator', */ 'private _factory': null, + /** + * Bucket diff store + * @type {Store} + */ + 'private _store_factory': null, + /** * Initialize validator @@ -62,12 +68,35 @@ module.exports = Class( 'DataValidator', * @param {BucketDataValidator} bucket_validator data validator * @param {ValidStateMonitor} field_monitor field state monitor * @param {ClientDependencyFactory} dep_factory REMOVE ME + * @param {function()} store_factory factory for diff store */ - __construct( bucket_validator, field_monitor, dep_factory ) + __construct( + bucket_validator, field_monitor, dep_factory, store_factory + ) { + if ( typeof store_factory !== 'function' ) + { + throw TypeError( "Expected function for parameter store_factory" ); + } + this._bucket_validator = bucket_validator; this._field_monitor = field_monitor; this._factory = dep_factory; + + this._createStores( store_factory ); + }, + + + /** + * Create internal diff stores + * + * @param {function()} store_factory function to produce stores + * + * @return {undefined} + */ + 'private _createStores': function( store_factory ) + { + this._bucket_store = store_factory(); }, @@ -89,7 +118,7 @@ module.exports = Class( 'DataValidator', let failures = {}; - this._bucket_validator.validate( diff, ( name, value, i ) => + _self._bucket_validator.validate( diff, ( name, value, i ) => { diff[ name ][ i ] = undefined; @@ -100,6 +129,53 @@ module.exports = Class( 'DataValidator', validatef && validatef( diff, failures ); // XXX: this assumes that the above is synchronous - return this._field_monitor.update( diff, failures ); + return this.updateFailures( diff, failures ); + }, + + + /** + * Update failures from external validation + * + * TODO: This is a transitional API---we should handle all validations, + * not allow external systems to meddle in our affairs. + * + * @param {Object} diff bucket diff + * @param {Object} failures failures per field name and index + * + * @return {Promise} promise to populate internal store + */ + 'public updateFailures'( diff, failures ) + { + const _self = this; + + return this._populateStore( diff ).then( () => + { + return _self._field_monitor.update( + _self._bucket_store, failures + ); + } ); + }, + + + /** + * Populate store with diff + * + * This effectively converts a basic array into a `Store`. This is + * surprisingly performant on v8. If the stores mix in traits, there + * may be a slight performance hit for trait-overridden methods. + * + * @param {Object} diff bucket diff + * + * @return {Promise} when all items have been added to the store + */ + 'private _populateStore'( diff ) + { + var bstore = this._bucket_store; + + return Promise.all( + Object.keys( diff ).map( + key => bstore.add( key, diff[ key ] ) + ) + ); }, } ); diff --git a/src/validate/ValidStateMonitor.js b/src/validate/ValidStateMonitor.js index a2b530f..5125967 100644 --- a/src/validate/ValidStateMonitor.js +++ b/src/validate/ValidStateMonitor.js @@ -19,9 +19,10 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - EventEmitter = require( 'events' ).EventEmitter, - Failure = require( './Failure' ); +var Class = require( 'easejs' ).Class; +var EventEmitter = require( 'events' ).EventEmitter; +var Failure = require( './Failure' ); +var Store = require( '../store/Store' ); /** @@ -50,9 +51,8 @@ module.exports = Class( 'ValidStateMonitor' ) * should omitted from the value if they are not failures. * * The return value is a promise that is accepted once all fix checks - * have been performed (after which the `fix` event is emitted if - * appropriate). The `failure` event is emitted synchronously if any - * additional failures are detected. + * have been performed. The `failure` event is always emitted _before_ + * the fix event. * * @param {Object} data key-value field data * @param {Object} failures key-value field errors @@ -61,19 +61,25 @@ module.exports = Class( 'ValidStateMonitor' ) */ 'public update': function( data, failures ) { - var _self = this; - - var fixed = this.detectFixes( data, this._failures, failures ), - count_new = this.mergeFailures( this._failures, failures ); - - if ( this.hasFailures() && ( count_new > 0 ) ) + if ( !Class.isA( Store, data ) ) { - this.emit( 'failure', this._failures ); + throw TypeError( + 'Bucket diff data must be a Store; given ' + data + ); } - // failures is synchronous, fixes async + var _self = this; + var fixed = this.detectFixes( data, this._failures, failures ); + return fixed.then( function( fixes ) { + var count_new = _self.mergeFailures( _self._failures, failures ); + + if ( _self.hasFailures() && ( count_new > 0 ) ) + { + _self.emit( 'failure', _self._failures ); + } + if ( fixes !== null ) { _self.emit( 'fix', fixes ); @@ -238,50 +244,98 @@ module.exports = Class( 'ValidStateMonitor' ) */ 'private _checkFailureFix': function( name, fail, past_fail, data, fixed ) { - var has_fixed = false; + var _self = this; // we must check each individual index because it is possible that // not every index was modified or fixed (we must loop through like // this because this is treated as a hash table, not an array) - for ( var i in past_fail ) + return Promise.all( past_fail.map( function( failure, fail_i ) { - var causes = past_fail[ i ] && past_fail[ i ].getCauses(); + var causes = failure && failure.getCauses() || []; - for ( var cause_i in causes ) - { - var cause = causes[ cause_i ], - cause_name = cause.getName(), - cause_index = cause.getIndex(), - field = data[ cause_name ]; - - // if datum is unchanged, ignore it - if ( field === undefined ) - { - continue; - } - - // to be marked as fixed, there must both me no failure and - // there must be data for this index for the field in question - // (if the field wasn't touched, then of course there's no - // failure!) - if ( ( fail === undefined ) - || ( !( fail[ cause_index ] ) - && ( field[ cause_index ] !== undefined ) ) + // to short-circuit checks, the promise will be _rejected_ once + // a match is found (see catch block) + return causes + .reduce( + _self._checkCauseFix.bind( _self, data, fail ), + Promise.resolve( true ) ) + .then( function() { + // no fixes + return false; + } ) + .catch( function( result ) + { + if ( result instanceof Error ) + { + throw result; + } + // looks like it has been resolved - ( fixed[ name ] = fixed[ name ] || [] )[ i ] = - field[ cause_index ] + ( fixed[ name ] = fixed[ name ] || [] )[ fail_i ] = result; - has_fixed = true; + delete past_fail[ fail_i ]; + return true; + } ); + } ) ).then( function( result ) { + return result.some( function( val ) + { + return val === true; + } ); + } ); + }, - delete past_fail[ i ]; - break; - } - } - } - // preparation for future use of Store, which is async - return Promise.resolve( has_fixed ); - } + /** + * Check past failure causes + * + * Each past failure in `fail` will be checked against the data in + * `diff` to determine whether it should be considered a possible + * fix. If so, the promise is fulfilled with the fix data. It is the + * responsibility of the caller to handle removing past failures. + * + * @param {Object} data validated data + * @param {Object} fail failure records + * @param {Promise} causep cause promise to chain onto + * @param {Field} cause field that caused the error + * + * @return {Promise} whether a field should be fixed + */ + 'private _checkCauseFix': function( data, fail, causep, cause ) + { + var cause_name = cause.getName(); + var cause_index = cause.getIndex(); + + return causep.then( function() + { + return new Promise( function( keepgoing, found ) + { + data.get( cause_name ).then( function( field ) + { + // to be marked as fixed, there must both me no failure + // and there must be data for this index for the field + // in question (if the field wasn't touched, then of + // course there's no failure!) + if ( ( fail === undefined ) + || ( !( fail[ cause_index ] ) + && ( field[ cause_index ] !== undefined ) ) + ) + { + found( field[ cause_index ] ); + return; + } + + // keep searching + keepgoing( true ); + } ) + .catch( function( e ) + { + // doesn't exist, so just keep searching (it + // wasn't fixed) + keepgoing( true ); + } ); + } ); + } ); + }, } ); diff --git a/test/validate/DataValidatorTest.js b/test/validate/DataValidatorTest.js index a209486..62478ee 100644 --- a/test/validate/DataValidatorTest.js +++ b/test/validate/DataValidatorTest.js @@ -21,12 +21,13 @@ "use strict"; -const root = require( '../../' ); -const validate = root.validate; -const Sut = validate.DataValidator; -const chai = require( 'chai' ); -const expect = chai.expect; -const sinon = require( 'sinon' ); +const root = require( '../../' ); +const validate = root.validate; +const Sut = validate.DataValidator; +const MemoryStore = root.store.MemoryStore; +const chai = require( 'chai' ); +const expect = chai.expect; +const sinon = require( 'sinon' ); const BucketDataValidator = validate.BucketDataValidator, ValidStateMonitor = validate.ValidStateMonitor; @@ -55,6 +56,7 @@ describe( 'DataValidator', () => const vmonitor = ValidStateMonitor(); const dep_factory = createMockDependencyFactory(); + const getStore = createStubStore(); const mock_vmonitor = sinon.mock( vmonitor ); const mock_dep_factory = sinon.mock( dep_factory ); @@ -68,7 +70,7 @@ describe( 'DataValidator', () => mock_vmonitor.expects( 'update' ) .once() - .withExactArgs( diff, expected_failures ) + .withExactArgs( getStore(), expected_failures ) .returns( Promise.resolve( undefined ) ); mock_dep_factory.expects( 'createFieldFailure' ) @@ -76,19 +78,17 @@ describe( 'DataValidator', () => .withExactArgs( 'foo', 1, expected_value ) .returns( expected_failure ); - const retp = Sut( bvalidator, vmonitor, dep_factory ) - .validate( diff ); + return Sut( bvalidator, vmonitor, dep_factory, getStore ) + .validate( diff ) + .then( () => + { + mock_vmonitor.verify(); + mock_dep_factory.verify(); - // cleared on call to err in above mock validator - expect( diff.foo ).to.deep.equal( - [ 'a', undefined, 'c' ] - ); - - mock_vmonitor.verify(); - mock_dep_factory.verify(); - - // the promise - return retp; + // cleared on call to err in above mock validator + return expect( getStore().get( 'foo' ) ) + .to.eventually.deep.equal( [ 'a', undefined, 'c' ] ); + } ); } ); @@ -106,6 +106,7 @@ describe( 'DataValidator', () => const vmonitor = ValidStateMonitor(); const dep_factory = createMockDependencyFactory(); + const getStore = createStubStore(); const diff = { foo: [ 'a', 'b', 'c' ] }; const expected_failures = { @@ -129,14 +130,14 @@ describe( 'DataValidator', () => sinon.mock( vmonitor ) .expects( 'update' ) .once() - .withExactArgs( diff, expected_failures ) + .withExactArgs( getStore(), expected_failures ) .returns( Promise.resolve( undefined ) ); sinon.mock( dep_factory ) .expects( 'createFieldFailure' ) .returns( expected_failure ); - return Sut( bvalidator, vmonitor, dep_factory ) + return Sut( bvalidator, vmonitor, dep_factory, getStore ) .validate( diff, validatef ); } ); @@ -155,7 +156,7 @@ describe( 'DataValidator', () => .returns( Promise.reject( expected_e ) ); return expect( - Sut( bvalidator, vmonitor, dep_factory ) + Sut( bvalidator, vmonitor, dep_factory, createStubStore() ) .validate( {} ) ).to.eventually.be.rejectedWith( expected_e ); } ); @@ -181,3 +182,11 @@ function createMockDependencyFactory( map ) createFieldFailure: () => {}, }; } + + +function createStubStore() +{ + const store = MemoryStore(); + + return () => store; +} diff --git a/test/validate/ValidStateMonitorTest.js b/test/validate/ValidStateMonitorTest.js index c129cdd..75dab73 100644 --- a/test/validate/ValidStateMonitorTest.js +++ b/test/validate/ValidStateMonitorTest.js @@ -19,17 +19,20 @@ * along with this program. If not, see . */ -var root = require( '../../' ), - Sut = root.validate.ValidStateMonitor, - chai = require( 'chai' ), - expect = chai.expect, - Failure = root.validate.Failure, - Field = root.field.BucketField; +"use strict"; + +const root = require( '../../' ); +const Sut = root.validate.ValidStateMonitor; +const chai = require( 'chai' ); +const expect = chai.expect; +const Failure = root.validate.Failure; +const Field = root.field.BucketField; +const MemoryStore = root.store.MemoryStore; chai.use( require( 'chai-as-promised' ) ); -var nocall = function( type ) +const nocall = function( type ) { return function() { @@ -37,7 +40,7 @@ var nocall = function( type ) }; }; -var mkfail = function( name, arr ) +const mkfail = function( name, arr ) { return arr.map( function( value, i ) { @@ -54,19 +57,25 @@ describe( 'ValidStateMonitor', function() { it( 'does nothing with no data or failures', function() { - return Sut() - .on( 'failure', nocall( 'failure' ) ) - .on( 'fix', nocall( 'fix' ) ) - .update( {}, {} ); + return mkstore( {} ).then( empty => + { + return Sut() + .on( 'failure', nocall( 'failure' ) ) + .on( 'fix', nocall( 'fix' ) ) + .update( empty, {} ); + } ); } ); it( 'does nothing with data but no failures', function() { - return Sut() - .on( 'failure', nocall( 'failure' ) ) - .on( 'fix', nocall( 'fix' ) ) - .update( { foo: mkfail( 'foo', [ 'bar' ] ) }, {} ); + return mkstore( { foo: mkfail( 'foo', [ 'bar' ] ) } ).then( store => + { + return Sut() + .on( 'failure', nocall( 'failure' ) ) + .on( 'fix', nocall( 'fix' ) ) + .update( store, {} ); + } ); } ); @@ -74,88 +83,118 @@ describe( 'ValidStateMonitor', function() // need the data describe( 'given failures', function() { - it( 'marks failures even when given no data', function( done ) + it( 'marks failures even when given no data', function() { var fail = mkfail( 'foo', [ 'bar', 'baz' ] ); - Sut() - .on( 'failure', function( failures ) + return mkstore( {} ).then( empty => + { + return new Promise( accept => { - expect( failures ) - .to.deep.equal( { foo: [ fail[ 0 ], fail[ 1 ] ] } ); - done(); - } ) - .on( 'fix', nocall( 'fix' ) ) - .update( {}, { foo: fail } ); + return Sut() + .on( 'failure', function( failures ) + { + expect( failures ) + .to.deep.equal( + { foo: [ fail[ 0 ], fail[ 1 ] ] } + ); + accept(); + } ) + .on( 'fix', nocall( 'fix' ) ) + .update( empty, { foo: fail } ); + } ); + } ); } ); - it( 'marks failures with index gaps', function( done ) + it( 'marks failures with index gaps', function() { var fail = mkfail( 'foo', [ undefined, 'baz' ] ); - Sut() - .on( 'failure', function( failures ) + return mkstore( {} ).then( empty => + { + return new Promise( accept => { - expect( failures ) - .to.deep.equal( { foo: [ undefined, fail[ 1 ] ] } ); - done(); - } ) - .on( 'fix', nocall( 'fix' ) ) - .update( {}, { foo: fail } ); + Sut() + .on( 'failure', function( failures ) + { + expect( failures ) + .to.deep.equal( + { foo: [ undefined, fail[ 1 ] ] } + ); + accept(); + } ) + .on( 'fix', nocall( 'fix' ) ) + .update( empty, { foo: fail } ); + } ); + } ); } ); - it( 'retains past failures when setting new', function( done ) + it( 'retains past failures when setting new', function() { var sut = Sut(), fail = mkfail( 'foo', [ 'bar', 'baz' ] ); - var test_first = function( failures ) + return new Promise( ( accept, reject ) => { - expect( failures ) - .to.deep.equal( { foo: [ undefined, fail[ 1 ] ] } ); - - sut.once( 'failure', test_second ); - }; - - var test_second = function( failures ) - { - expect( failures ) - .to.deep.equal( { foo: [ fail[ 0 ], fail[ 1 ] ] } ); - - done(); - }; - - sut - .once( 'failure', test_first ) - .on( 'fix', nocall( 'fix' ) ) - .update( {}, { foo: [ undefined, fail[ 1 ] ] } ) - .then( () => + var test_first = function( failures ) { - return sut.update( {}, { foo: [ fail[ 0 ] ] } ); - } ); + expect( failures ) + .to.deep.equal( { foo: [ undefined, fail[ 1 ] ] } ); + + sut.once( 'failure', test_second ); + }; + + var test_second = function( failures ) + { + expect( failures ) + .to.deep.equal( { foo: [ fail[ 0 ], fail[ 1 ] ] } ); + + accept(); + }; + + mkstore( {} ).then( empty => + { + return sut + .once( 'failure', test_first ) + .on( 'fix', nocall( 'fix' ) ) + .update( empty, { foo: [ undefined, fail[ 1 ] ] } ) + .then( () => + { + return sut.update( empty, { foo: [ fail[ 0 ] ] } ); + } ); + } ).catch( e => reject( e ) ); + } ); } ); // deprecated - it( 'accepts failures as string for BC', function( done ) + it( 'accepts failures as string for BC', function() { var fail = [ 'foo', 'bar' ]; - Sut() - .on( 'failure', function( failures ) + return new Promise( ( accept, reject ) => + { + return mkstore( {} ).then( empty => { - expect( failures ) - .to.deep.equal( { foo: fail } ); - done(); + return Sut() + .on( 'failure', function( failures ) + { + expect( failures ) + .to.deep.equal( { foo: fail } ); + + accept(); + } ) + .on( 'fix', nocall( 'fix' ) ) + .update( empty, { foo: fail } ); } ) - .on( 'fix', nocall( 'fix' ) ) - .update( {}, { foo: fail } ); + .catch( e => reject( e ) ); + } ); } ); - it( 'does not discard existing failures', function( done ) + it( 'does not discard existing failures', function() { var sut = Sut(); @@ -176,79 +215,106 @@ describe( 'ValidStateMonitor', function() // the second failure has fewer causes than the first; // we need to make sure that it doesn't overwrite, // leading to fewer caues - sut - .update( {}, { foo: [ fail1 ] } ) - .then( () => - { - return sut.update( {}, { foo: [ fail2 ] } ); - } ) - .then( () => + return new Promise( ( accept, reject ) => + { + return mkstore( {} ).then( empty => { return sut - .once( 'fix', function( fixed ) + .update( empty, { foo: [ fail1 ] } ) + .then( () => { - expect( fixed ) - .to.deep.equal( { foo: [ 'causefix1' ] } ); - - // and then we should have no failures - expect( sut.hasFailures() ).to.be.false; - - done(); + return sut.update( empty, { foo: [ fail2 ] } ); } ) - .update( - { foo: [ 'moo' ], cause1: [ 'causefix1' ] }, - {} - ); - } ); + .then( () => + { + const update = { + foo: [ 'moo' ], + cause1: [ 'causefix1' ] + }; + + return mkstore( update ).then( store => + { + return sut + .once( 'fix', function( fixed ) + { + expect( fixed ).to.deep.equal( + { foo: [ 'causefix1' ] } + ); + + // and then we should have no failures + expect( sut.hasFailures() ) + .to.be.false; + + accept( true ); + } ) + .update( store, {} ); + } ); + } ); + } ) + .catch( e => reject( e ) ); + } ); } ); } ); describe( 'given data with absence of failure', function() { - it( 'removes non-failures if field is present', function( done ) + it( 'removes non-failures if field is present', function() { - var data = { foo: [ 'bardata', 'baz' ] }, - fail = mkfail( 'foo', [ 'bar', 'baz' ] ); + const fail = mkfail( 'foo', [ 'bar', 'baz' ] ); + const sut = Sut(); - var sut = Sut(); - - sut - .on( 'fix', function( fixed ) + return new Promise( ( accept, reject ) => + { + return mkstore( { foo: [ 'bardata', 'baz' ] } ).then( data => { - expect( fixed ) - .to.deep.equal( { foo: [ 'bardata' ] } ); - done(); + return sut + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( { foo: [ 'bardata' ] } ); + accept(); + } ) + .update( data, { foo: [ fail[ 0 ], fail[ 1 ] ] } ) + .then( () => + { + return sut.update( data, { + foo: [ undefined, fail[ 1 ] ] + } ); + } ); } ) - .update( data, { foo: [ fail[ 0 ], fail[ 1 ] ] } ) - .then( () => - { - return sut.update( data, { foo: [ undefined, fail[ 1 ] ] } ); - } ); + .catch( e => reject( e ) ); + } ); } ); - it( 'keeps failures if field is missing', function( done ) + it( 'keeps failures if field is missing', function() { - var data = { bar: [ 'baz', 'quux' ] }, - fail_foo = mkfail( 'foo', [ 'bar', 'baz' ] ), - fail_bar = mkfail( 'bar', [ 'moo', 'cow' ] ); + const fail_foo = mkfail( 'foo', [ 'bar', 'baz' ] ); + const fail_bar = mkfail( 'bar', [ 'moo', 'cow' ] ); - Sut() - .on( 'fix', function( fixed ) + return new Promise( ( accept, reject ) => + { + return mkstore( { bar: [ 'baz', 'quux' ] } ).then( data => { - expect( fixed ) - .to.deep.equal( { bar: [ 'baz', 'quux' ] } ); - done(); + return Sut() + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( { bar: [ 'baz', 'quux' ] } ); + accept(); + } ) + .update( data, { + foo: fail_foo, // does not exist in data + bar: fail_bar, + } ) + .then( sut => + { + return sut.update( data, {} ); + } ); } ) - .update( data, { - foo: fail_foo, // does not exist in data - bar: fail_bar, - } ) - .then( sut => - { - return sut.update( data, {} ); - } ); + .catch( e => reject( e ) ); + } ); } ); @@ -256,156 +322,199 @@ describe( 'ValidStateMonitor', function() { var called = 0; - Sut() - .on( 'failure', function() - { - called++; - } ) - .update( {}, { foo: mkfail( 'foo', [ 'bar' ] ) } ) - .then( sut => - { - return sut.update( {}, {} ); // do not trigger failure event - } ) - .then( sut => - { - expect( called ).to.equal( 1 ); - } ); + return mkstore( {} ).then( empty => + { + return Sut() + .on( 'failure', function() + { + called++; + } ) + .update( empty, { foo: mkfail( 'foo', [ 'bar' ] ) } ) + .then( sut => + { + return sut.update( empty, {} ); // do not trigger failure event + } ) + .then( sut => + { + expect( called ).to.equal( 1 ); + } ); + } ); } ); describe( 'given a cause', function() { - it( 'considers when recognizing fix', function( done ) + it( 'considers when recognizing fix', function() { // same index - var data = { cause: [ 'bar' ] }, - field = Field( 'foo', 0 ), - cause = Field( 'cause', 0 ), - fail = Failure( field, 'reason', [ cause ] ); + const field = Field( 'foo', 0 ); + const cause = Field( 'cause', 0 ); + const fail = Failure( field, 'reason', [ cause ] ); - Sut() - .on( 'fix', function( fixed ) + return new Promise( ( accept, reject ) => + { + return mkstore( { cause: [ 'bar' ] } ).then( data => { - expect( fixed ) - .to.deep.equal( { foo: [ 'bar' ] } ); + return Sut() + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( { foo: [ 'bar' ] } ); - done(); + accept(); + } ) + .update( data, { foo: [ fail ] } ) + .then( sut => + { + return sut.update( data, {} ); + } ); } ) - .update( data, { foo: [ fail ] } ) - .then( sut => - { - return sut.update( data, {} ); - } ); + .catch( e => reject( e ) ); + } ); } ); - it( 'considers different cause index', function( done ) + it( 'considers different cause index', function() { // different index - var data = { cause: [ undefined, 'bar' ] }, - field = Field( 'foo', 0 ), - cause = Field( 'cause', 1 ), - fail = Failure( field, 'reason', [ cause ] ); + const update_data = { cause: [ undefined, 'bar' ] }; + const field = Field( 'foo', 0 ); + const cause = Field( 'cause', 1 ); + const fail = Failure( field, 'reason', [ cause ] ); - Sut() - .on( 'fix', function( fixed ) + return new Promise( ( accept, reject ) => + { + return mkstore( update_data ).then( data => { - expect( fixed ) - .to.deep.equal( { foo: [ 'bar' ] } ); + return Sut() + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( { foo: [ 'bar' ] } ); - done(); + accept(); + } ) + .update( data, { foo: [ fail ] } ) + .then( sut => + { + return sut.update( data, {} ); + } ); } ) - .update( data, { foo: [ fail ] } ) - .then( sut => - { - return sut.update( data, {} ); - } ); + .catch( e => reject( e ) ); + } ); } ); - it( 'considers any number of causes', function( done ) + it( 'considers any number of causes', function() { // different index - var data = { cause_fix: [ undefined, 'bar' ] }, - field = Field( 'foo', 0 ), - cause1 = Field( 'cause_no', 1 ), - cause2 = Field( 'cause_fix', 1 ), - fail = Failure( - field, - 'reason', - [ cause1, cause2 ] - ); + const update_data = { cause_fix: [ undefined, 'bar' ] }; + const field = Field( 'foo', 0 ); + const cause1 = Field( 'cause_no', 1 ); + const cause2 = Field( 'cause_fix', 1 ); - Sut() - .on( 'fix', function( fixed ) + const fail = Failure( + field, + 'reason', + [ cause1, cause2 ] + ); + + return new Promise( ( accept, reject ) => + { + return mkstore( update_data ).then( data => { - expect( fixed ) - .to.deep.equal( { foo: [ 'bar' ] } ); + return Sut() + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( { foo: [ 'bar' ] } ); - done(); + accept(); + } ) + .update( data, { foo: [ fail ] } ) + .then( sut => + { + return sut.update( data, {} ); + } ); } ) - .update( data, { foo: [ fail ] } ) - .then( sut => - { - return sut.update( data, {} ); - } ); + .catch( e => reject( e ) ); + } ); } ); it( 'recognizes non-fix', function() { // no cause data - var data = { noncause: [ undefined, 'bar' ] }, - field = Field( 'foo', 0 ), - cause1 = Field( 'cause', 1 ), - cause2 = Field( 'cause', 2 ), - fail = Failure( - field, - 'reason', - [ cause1, cause2 ] - ); + const update_data = mkstore( { noncause: [ undefined, 'bar' ] } ); + const field = Field( 'foo', 0 ); + const cause1 = Field( 'cause', 1 ); + const cause2 = Field( 'cause', 2 ); - Sut() - .on( 'fix', nocall ) - .update( data, { foo: [ fail ] } ) - .then( sut => - { - return sut.update( data, {} ); - } ); + const fail = Failure( + field, + 'reason', + [ cause1, cause2 ] + ); + + return mkstore( update_data ).then( data => + { + return Sut() + .on( 'fix', nocall ) + .update( data, { foo: [ fail ] } ) + .then( sut => + { + return sut.update( data, {} ); + } ); + } ); } ); } ); } ); - it( 'can emit both failure and fix', function( done ) + it( 'can emit both failure and fix', function() { - var data = { bar: [ 'baz', 'quux' ] }, - fail_foo = mkfail( 'foo', [ 'bar' ] ); + var fail_foo = mkfail( 'foo', [ 'bar' ] ); - Sut() - .update( data, { - bar: mkfail( 'bar', [ 'moo', 'cow' ] ) // fail - } ) - .then( sut => - { - return sut.on( 'failure', function( failed ) - { - expect( failed ) - .to.deep.equal( { - foo: fail_foo, - } ); - } ) - .on( 'fix', function( fixed ) - { - expect( fixed ) - .to.deep.equal( { bar: [ 'baz', 'quux' ] } ); - done(); - } ) + return mkstore( { bar: [ 'baz', 'quux' ] } ).then( data => + { + return Sut() .update( data, { - foo: fail_foo, // fail - // fixes bar + bar: mkfail( 'bar', [ 'moo', 'cow' ] ) // fail + } ) + .then( sut => + { + return new Promise( ( accept, reject ) => + { + sut.on( 'failure', function( failed ) + { + expect( failed ) + .to.deep.equal( { + foo: fail_foo, + } ); + } ) + .on( 'fix', function( fixed ) + { + expect( fixed ) + .to.deep.equal( + { bar: [ 'baz', 'quux' ] } + ); + + // note that the documentation for #update + // states that failure will always be + // emitted before fix + accept( true ); + } ) + .update( data, { + foo: fail_foo, // fail + // fixes bar + } ) + .catch( e => + { + reject( e ); + } ); + } ); } ); - } ); + } ); } ); } ); @@ -425,14 +534,17 @@ describe( 'ValidStateMonitor', function() { var fail = mkfail( 'foo', [ 'fail' ] ); - return expect( - Sut() - .update( {}, { foo: fail } ) - .then( sut => - { - return sut.getFailures() - } ) - ).to.eventually.deep.equal( { foo: fail } ); + return mkstore( {} ).then( empty => + { + return expect( + Sut() + .update( empty, { foo: fail } ) + .then( sut => + { + return sut.getFailures() + } ) + ).to.eventually.deep.equal( { foo: fail } ); + } ); } ); } ); @@ -448,14 +560,27 @@ describe( 'ValidStateMonitor', function() it( 'is true when failures exist', function() { - return expect( - Sut() - .update( {}, { foo: mkfail( 'foo', [ 'bar' ] ) } ) - .then( sut => - { - return sut.hasFailures(); - } ) - ).to.eventually.be.true; + return mkstore( {} ).then( empty => + { + return expect( + Sut() + .update( empty, { foo: mkfail( 'foo', [ 'bar' ] ) } ) + .then( sut => + { + return sut.hasFailures(); + } ) + ).to.eventually.be.true; + } ); } ); } ); } ); + + +function mkstore( data ) +{ + let store = MemoryStore(); + + return Promise.all( + Object.keys( data ).map( key => store.add( key, data[ key ] ) ) + ).then( () => store ); +}