1
0
Fork 0

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
master
Mike Gerwitz 2017-01-26 16:03:00 -05:00
parent 203b25f10e
commit 2045c76f7e
4 changed files with 576 additions and 312 deletions

View File

@ -55,6 +55,12 @@ module.exports = Class( 'DataValidator',
*/ */
'private _factory': null, 'private _factory': null,
/**
* Bucket diff store
* @type {Store}
*/
'private _store_factory': null,
/** /**
* Initialize validator * Initialize validator
@ -62,12 +68,35 @@ module.exports = Class( 'DataValidator',
* @param {BucketDataValidator} bucket_validator data validator * @param {BucketDataValidator} bucket_validator data validator
* @param {ValidStateMonitor} field_monitor field state monitor * @param {ValidStateMonitor} field_monitor field state monitor
* @param {ClientDependencyFactory} dep_factory REMOVE ME * @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._bucket_validator = bucket_validator;
this._field_monitor = field_monitor; this._field_monitor = field_monitor;
this._factory = dep_factory; 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 = {}; let failures = {};
this._bucket_validator.validate( diff, ( name, value, i ) => _self._bucket_validator.validate( diff, ( name, value, i ) =>
{ {
diff[ name ][ i ] = undefined; diff[ name ][ i ] = undefined;
@ -100,6 +129,53 @@ module.exports = Class( 'DataValidator',
validatef && validatef( diff, failures ); validatef && validatef( diff, failures );
// XXX: this assumes that the above is synchronous // 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 ] )
)
);
}, },
} ); } );

View File

@ -19,9 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var Class = require( 'easejs' ).Class, var Class = require( 'easejs' ).Class;
EventEmitter = require( 'events' ).EventEmitter, var EventEmitter = require( 'events' ).EventEmitter;
Failure = require( './Failure' ); 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. * should omitted from the value if they are not failures.
* *
* The return value is a promise that is accepted once all fix checks * The return value is a promise that is accepted once all fix checks
* have been performed (after which the `fix` event is emitted if * have been performed. The `failure` event is always emitted _before_
* appropriate). The `failure` event is emitted synchronously if any * the fix event.
* additional failures are detected.
* *
* @param {Object} data key-value field data * @param {Object} data key-value field data
* @param {Object} failures key-value field errors * @param {Object} failures key-value field errors
@ -61,19 +61,25 @@ module.exports = Class( 'ValidStateMonitor' )
*/ */
'public update': function( data, failures ) 'public update': function( data, failures )
{ {
var _self = this; if ( !Class.isA( Store, data ) )
var fixed = this.detectFixes( data, this._failures, failures ),
count_new = this.mergeFailures( this._failures, failures );
if ( this.hasFailures() && ( count_new > 0 ) )
{ {
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 ) 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 ) if ( fixes !== null )
{ {
_self.emit( 'fix', fixes ); _self.emit( 'fix', fixes );
@ -238,50 +244,98 @@ module.exports = Class( 'ValidStateMonitor' )
*/ */
'private _checkFailureFix': function( name, fail, past_fail, data, fixed ) '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 // we must check each individual index because it is possible that
// not every index was modified or fixed (we must loop through like // not every index was modified or fixed (we must loop through like
// this because this is treated as a hash table, not an array) // 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 ) // to short-circuit checks, the promise will be _rejected_ once
{ // a match is found (see catch block)
var cause = causes[ cause_i ], return causes
cause_name = cause.getName(), .reduce(
cause_index = cause.getIndex(), _self._checkCauseFix.bind( _self, data, fail ),
field = data[ cause_name ]; Promise.resolve( true )
// 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 ) )
) )
.then( function()
{ {
// no fixes
return false;
} )
.catch( function( result )
{
if ( result instanceof Error )
{
throw result;
}
// looks like it has been resolved // looks like it has been resolved
( fixed[ name ] = fixed[ name ] || [] )[ i ] = ( fixed[ name ] = fixed[ name ] || [] )[ fail_i ] = result;
field[ cause_index ]
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 );
} );
} );
} );
},
} ); } );

View File

@ -21,12 +21,13 @@
"use strict"; "use strict";
const root = require( '../../' ); const root = require( '../../' );
const validate = root.validate; const validate = root.validate;
const Sut = validate.DataValidator; const Sut = validate.DataValidator;
const chai = require( 'chai' ); const MemoryStore = root.store.MemoryStore;
const expect = chai.expect; const chai = require( 'chai' );
const sinon = require( 'sinon' ); const expect = chai.expect;
const sinon = require( 'sinon' );
const BucketDataValidator = validate.BucketDataValidator, const BucketDataValidator = validate.BucketDataValidator,
ValidStateMonitor = validate.ValidStateMonitor; ValidStateMonitor = validate.ValidStateMonitor;
@ -55,6 +56,7 @@ describe( 'DataValidator', () =>
const vmonitor = ValidStateMonitor(); const vmonitor = ValidStateMonitor();
const dep_factory = createMockDependencyFactory(); const dep_factory = createMockDependencyFactory();
const getStore = createStubStore();
const mock_vmonitor = sinon.mock( vmonitor ); const mock_vmonitor = sinon.mock( vmonitor );
const mock_dep_factory = sinon.mock( dep_factory ); const mock_dep_factory = sinon.mock( dep_factory );
@ -68,7 +70,7 @@ describe( 'DataValidator', () =>
mock_vmonitor.expects( 'update' ) mock_vmonitor.expects( 'update' )
.once() .once()
.withExactArgs( diff, expected_failures ) .withExactArgs( getStore(), expected_failures )
.returns( Promise.resolve( undefined ) ); .returns( Promise.resolve( undefined ) );
mock_dep_factory.expects( 'createFieldFailure' ) mock_dep_factory.expects( 'createFieldFailure' )
@ -76,19 +78,17 @@ describe( 'DataValidator', () =>
.withExactArgs( 'foo', 1, expected_value ) .withExactArgs( 'foo', 1, expected_value )
.returns( expected_failure ); .returns( expected_failure );
const retp = Sut( bvalidator, vmonitor, dep_factory ) return Sut( bvalidator, vmonitor, dep_factory, getStore )
.validate( diff ); .validate( diff )
.then( () =>
{
mock_vmonitor.verify();
mock_dep_factory.verify();
// cleared on call to err in above mock validator // cleared on call to err in above mock validator
expect( diff.foo ).to.deep.equal( return expect( getStore().get( 'foo' ) )
[ 'a', undefined, 'c' ] .to.eventually.deep.equal( [ 'a', undefined, 'c' ] );
); } );
mock_vmonitor.verify();
mock_dep_factory.verify();
// the promise
return retp;
} ); } );
@ -106,6 +106,7 @@ describe( 'DataValidator', () =>
const vmonitor = ValidStateMonitor(); const vmonitor = ValidStateMonitor();
const dep_factory = createMockDependencyFactory(); const dep_factory = createMockDependencyFactory();
const getStore = createStubStore();
const diff = { foo: [ 'a', 'b', 'c' ] }; const diff = { foo: [ 'a', 'b', 'c' ] };
const expected_failures = { const expected_failures = {
@ -129,14 +130,14 @@ describe( 'DataValidator', () =>
sinon.mock( vmonitor ) sinon.mock( vmonitor )
.expects( 'update' ) .expects( 'update' )
.once() .once()
.withExactArgs( diff, expected_failures ) .withExactArgs( getStore(), expected_failures )
.returns( Promise.resolve( undefined ) ); .returns( Promise.resolve( undefined ) );
sinon.mock( dep_factory ) sinon.mock( dep_factory )
.expects( 'createFieldFailure' ) .expects( 'createFieldFailure' )
.returns( expected_failure ); .returns( expected_failure );
return Sut( bvalidator, vmonitor, dep_factory ) return Sut( bvalidator, vmonitor, dep_factory, getStore )
.validate( diff, validatef ); .validate( diff, validatef );
} ); } );
@ -155,7 +156,7 @@ describe( 'DataValidator', () =>
.returns( Promise.reject( expected_e ) ); .returns( Promise.reject( expected_e ) );
return expect( return expect(
Sut( bvalidator, vmonitor, dep_factory ) Sut( bvalidator, vmonitor, dep_factory, createStubStore() )
.validate( {} ) .validate( {} )
).to.eventually.be.rejectedWith( expected_e ); ).to.eventually.be.rejectedWith( expected_e );
} ); } );
@ -181,3 +182,11 @@ function createMockDependencyFactory( map )
createFieldFailure: () => {}, createFieldFailure: () => {},
}; };
} }
function createStubStore()
{
const store = MemoryStore();
return () => store;
}

View File

@ -19,17 +19,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var root = require( '../../' ), "use strict";
Sut = root.validate.ValidStateMonitor,
chai = require( 'chai' ), const root = require( '../../' );
expect = chai.expect, const Sut = root.validate.ValidStateMonitor;
Failure = root.validate.Failure, const chai = require( 'chai' );
Field = root.field.BucketField; 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' ) ); chai.use( require( 'chai-as-promised' ) );
var nocall = function( type ) const nocall = function( type )
{ {
return function() 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 ) return arr.map( function( value, i )
{ {
@ -54,19 +57,25 @@ describe( 'ValidStateMonitor', function()
{ {
it( 'does nothing with no data or failures', function() it( 'does nothing with no data or failures', function()
{ {
return Sut() return mkstore( {} ).then( empty =>
.on( 'failure', nocall( 'failure' ) ) {
.on( 'fix', nocall( 'fix' ) ) return Sut()
.update( {}, {} ); .on( 'failure', nocall( 'failure' ) )
.on( 'fix', nocall( 'fix' ) )
.update( empty, {} );
} );
} ); } );
it( 'does nothing with data but no failures', function() it( 'does nothing with data but no failures', function()
{ {
return Sut() return mkstore( { foo: mkfail( 'foo', [ 'bar' ] ) } ).then( store =>
.on( 'failure', nocall( 'failure' ) ) {
.on( 'fix', nocall( 'fix' ) ) return Sut()
.update( { foo: mkfail( 'foo', [ 'bar' ] ) }, {} ); .on( 'failure', nocall( 'failure' ) )
.on( 'fix', nocall( 'fix' ) )
.update( store, {} );
} );
} ); } );
@ -74,88 +83,118 @@ describe( 'ValidStateMonitor', function()
// need the data // need the data
describe( 'given failures', function() 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' ] ); var fail = mkfail( 'foo', [ 'bar', 'baz' ] );
Sut() return mkstore( {} ).then( empty =>
.on( 'failure', function( failures ) {
return new Promise( accept =>
{ {
expect( failures ) return Sut()
.to.deep.equal( { foo: [ fail[ 0 ], fail[ 1 ] ] } ); .on( 'failure', function( failures )
done(); {
} ) expect( failures )
.on( 'fix', nocall( 'fix' ) ) .to.deep.equal(
.update( {}, { foo: fail } ); { 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' ] ); var fail = mkfail( 'foo', [ undefined, 'baz' ] );
Sut() return mkstore( {} ).then( empty =>
.on( 'failure', function( failures ) {
return new Promise( accept =>
{ {
expect( failures ) Sut()
.to.deep.equal( { foo: [ undefined, fail[ 1 ] ] } ); .on( 'failure', function( failures )
done(); {
} ) expect( failures )
.on( 'fix', nocall( 'fix' ) ) .to.deep.equal(
.update( {}, { foo: fail } ); { 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(), var sut = Sut(),
fail = mkfail( 'foo', [ 'bar', 'baz' ] ); fail = mkfail( 'foo', [ 'bar', 'baz' ] );
var test_first = function( failures ) return new Promise( ( accept, reject ) =>
{ {
expect( failures ) var test_first = function( 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( () =>
{ {
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 // deprecated
it( 'accepts failures as string for BC', function( done ) it( 'accepts failures as string for BC', function()
{ {
var fail = [ 'foo', 'bar' ]; var fail = [ 'foo', 'bar' ];
Sut() return new Promise( ( accept, reject ) =>
.on( 'failure', function( failures ) {
return mkstore( {} ).then( empty =>
{ {
expect( failures ) return Sut()
.to.deep.equal( { foo: fail } ); .on( 'failure', function( failures )
done(); {
expect( failures )
.to.deep.equal( { foo: fail } );
accept();
} )
.on( 'fix', nocall( 'fix' ) )
.update( empty, { foo: fail } );
} ) } )
.on( 'fix', nocall( 'fix' ) ) .catch( e => reject( e ) );
.update( {}, { foo: fail } ); } );
} ); } );
it( 'does not discard existing failures', function( done ) it( 'does not discard existing failures', function()
{ {
var sut = Sut(); var sut = Sut();
@ -176,79 +215,106 @@ describe( 'ValidStateMonitor', function()
// the second failure has fewer causes than the first; // the second failure has fewer causes than the first;
// we need to make sure that it doesn't overwrite, // we need to make sure that it doesn't overwrite,
// leading to fewer caues // leading to fewer caues
sut return new Promise( ( accept, reject ) =>
.update( {}, { foo: [ fail1 ] } ) {
.then( () => return mkstore( {} ).then( empty =>
{
return sut.update( {}, { foo: [ fail2 ] } );
} )
.then( () =>
{ {
return sut return sut
.once( 'fix', function( fixed ) .update( empty, { foo: [ fail1 ] } )
.then( () =>
{ {
expect( fixed ) return sut.update( empty, { foo: [ fail2 ] } );
.to.deep.equal( { foo: [ 'causefix1' ] } );
// and then we should have no failures
expect( sut.hasFailures() ).to.be.false;
done();
} ) } )
.update( .then( () =>
{ foo: [ 'moo' ], cause1: [ 'causefix1' ] }, {
{} 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() 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' ] }, const fail = mkfail( 'foo', [ 'bar', 'baz' ] );
fail = mkfail( 'foo', [ 'bar', 'baz' ] ); const sut = Sut();
var sut = Sut(); return new Promise( ( accept, reject ) =>
{
sut return mkstore( { foo: [ 'bardata', 'baz' ] } ).then( data =>
.on( 'fix', function( fixed )
{ {
expect( fixed ) return sut
.to.deep.equal( { foo: [ 'bardata' ] } ); .on( 'fix', function( fixed )
done(); {
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 ] ] } ) .catch( e => reject( e ) );
.then( () => } );
{
return sut.update( data, { foo: [ undefined, fail[ 1 ] ] } );
} );
} ); } );
it( 'keeps failures if field is missing', function( done ) it( 'keeps failures if field is missing', function()
{ {
var data = { bar: [ 'baz', 'quux' ] }, const fail_foo = mkfail( 'foo', [ 'bar', 'baz' ] );
fail_foo = mkfail( 'foo', [ 'bar', 'baz' ] ), const fail_bar = mkfail( 'bar', [ 'moo', 'cow' ] );
fail_bar = mkfail( 'bar', [ 'moo', 'cow' ] );
Sut() return new Promise( ( accept, reject ) =>
.on( 'fix', function( fixed ) {
return mkstore( { bar: [ 'baz', 'quux' ] } ).then( data =>
{ {
expect( fixed ) return Sut()
.to.deep.equal( { bar: [ 'baz', 'quux' ] } ); .on( 'fix', function( fixed )
done(); {
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, { .catch( e => reject( e ) );
foo: fail_foo, // does not exist in data } );
bar: fail_bar,
} )
.then( sut =>
{
return sut.update( data, {} );
} );
} ); } );
@ -256,156 +322,199 @@ describe( 'ValidStateMonitor', function()
{ {
var called = 0; var called = 0;
Sut() return mkstore( {} ).then( empty =>
.on( 'failure', function() {
{ return Sut()
called++; .on( 'failure', function()
} ) {
.update( {}, { foo: mkfail( 'foo', [ 'bar' ] ) } ) called++;
.then( sut => } )
{ .update( empty, { foo: mkfail( 'foo', [ 'bar' ] ) } )
return sut.update( {}, {} ); // do not trigger failure event .then( sut =>
} ) {
.then( sut => return sut.update( empty, {} ); // do not trigger failure event
{ } )
expect( called ).to.equal( 1 ); .then( sut =>
} ); {
expect( called ).to.equal( 1 );
} );
} );
} ); } );
describe( 'given a cause', function() describe( 'given a cause', function()
{ {
it( 'considers when recognizing fix', function( done ) it( 'considers when recognizing fix', function()
{ {
// same index // same index
var data = { cause: [ 'bar' ] }, const field = Field( 'foo', 0 );
field = Field( 'foo', 0 ), const cause = Field( 'cause', 0 );
cause = Field( 'cause', 0 ), const fail = Failure( field, 'reason', [ cause ] );
fail = Failure( field, 'reason', [ cause ] );
Sut() return new Promise( ( accept, reject ) =>
.on( 'fix', function( fixed ) {
return mkstore( { cause: [ 'bar' ] } ).then( data =>
{ {
expect( fixed ) return Sut()
.to.deep.equal( { foo: [ 'bar' ] } ); .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 ] } ) .catch( e => reject( e ) );
.then( sut => } );
{
return sut.update( data, {} );
} );
} ); } );
it( 'considers different cause index', function( done ) it( 'considers different cause index', function()
{ {
// different index // different index
var data = { cause: [ undefined, 'bar' ] }, const update_data = { cause: [ undefined, 'bar' ] };
field = Field( 'foo', 0 ), const field = Field( 'foo', 0 );
cause = Field( 'cause', 1 ), const cause = Field( 'cause', 1 );
fail = Failure( field, 'reason', [ cause ] ); const fail = Failure( field, 'reason', [ cause ] );
Sut() return new Promise( ( accept, reject ) =>
.on( 'fix', function( fixed ) {
return mkstore( update_data ).then( data =>
{ {
expect( fixed ) return Sut()
.to.deep.equal( { foo: [ 'bar' ] } ); .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 ] } ) .catch( e => reject( e ) );
.then( sut => } );
{
return sut.update( data, {} );
} );
} ); } );
it( 'considers any number of causes', function( done ) it( 'considers any number of causes', function()
{ {
// different index // different index
var data = { cause_fix: [ undefined, 'bar' ] }, const update_data = { cause_fix: [ undefined, 'bar' ] };
field = Field( 'foo', 0 ), const field = Field( 'foo', 0 );
cause1 = Field( 'cause_no', 1 ), const cause1 = Field( 'cause_no', 1 );
cause2 = Field( 'cause_fix', 1 ), const cause2 = Field( 'cause_fix', 1 );
fail = Failure(
field,
'reason',
[ cause1, cause2 ]
);
Sut() const fail = Failure(
.on( 'fix', function( fixed ) field,
'reason',
[ cause1, cause2 ]
);
return new Promise( ( accept, reject ) =>
{
return mkstore( update_data ).then( data =>
{ {
expect( fixed ) return Sut()
.to.deep.equal( { foo: [ 'bar' ] } ); .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 ] } ) .catch( e => reject( e ) );
.then( sut => } );
{
return sut.update( data, {} );
} );
} ); } );
it( 'recognizes non-fix', function() it( 'recognizes non-fix', function()
{ {
// no cause data // no cause data
var data = { noncause: [ undefined, 'bar' ] }, const update_data = mkstore( { noncause: [ undefined, 'bar' ] } );
field = Field( 'foo', 0 ), const field = Field( 'foo', 0 );
cause1 = Field( 'cause', 1 ), const cause1 = Field( 'cause', 1 );
cause2 = Field( 'cause', 2 ), const cause2 = Field( 'cause', 2 );
fail = Failure(
field,
'reason',
[ cause1, cause2 ]
);
Sut() const fail = Failure(
.on( 'fix', nocall ) field,
.update( data, { foo: [ fail ] } ) 'reason',
.then( sut => [ cause1, cause2 ]
{ );
return sut.update( data, {} );
} ); 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' ] }, var fail_foo = mkfail( 'foo', [ 'bar' ] );
fail_foo = mkfail( 'foo', [ 'bar' ] );
Sut() return mkstore( { bar: [ 'baz', 'quux' ] } ).then( data =>
.update( data, { {
bar: mkfail( 'bar', [ 'moo', 'cow' ] ) // fail return Sut()
} )
.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();
} )
.update( data, { .update( data, {
foo: fail_foo, // fail bar: mkfail( 'bar', [ 'moo', 'cow' ] ) // fail
// fixes bar } )
.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' ] ); var fail = mkfail( 'foo', [ 'fail' ] );
return expect( return mkstore( {} ).then( empty =>
Sut() {
.update( {}, { foo: fail } ) return expect(
.then( sut => Sut()
{ .update( empty, { foo: fail } )
return sut.getFailures() .then( sut =>
} ) {
).to.eventually.deep.equal( { foo: fail } ); return sut.getFailures()
} )
).to.eventually.deep.equal( { foo: fail } );
} );
} ); } );
} ); } );
@ -448,14 +560,27 @@ describe( 'ValidStateMonitor', function()
it( 'is true when failures exist', function() it( 'is true when failures exist', function()
{ {
return expect( return mkstore( {} ).then( empty =>
Sut() {
.update( {}, { foo: mkfail( 'foo', [ 'bar' ] ) } ) return expect(
.then( sut => Sut()
{ .update( empty, { foo: mkfail( 'foo', [ 'bar' ] ) } )
return sut.hasFailures(); .then( sut =>
} ) {
).to.eventually.be.true; 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 );
}