diff --git a/Makefile b/Makefile index 428c3f1..6247746 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ path_perf_test=${path_test}/perf perf_tests := $(shell find "$(path_perf_test)" -name 'perf-*.js') +src_js := index.js $(wildcard $(path_lib)/*.js) +src_tests := index.js $(wildcard $(path_test)/test-*) + path_doc := ./doc combine=${path_tools}/combine diff --git a/doc/Makefile b/doc/Makefile index 87661ba..87e3425 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -15,8 +15,6 @@ path_manual_texi=${path_doc}/manual.texi path_info_install := /usr/local/share/info -src_js := index.js $(wildcard $(path_lib)/*.js) -src_tests := index.js $(wildcard $(path_test)/test-*) doc_src := $(wildcard $(path_doc)/*.texi) doc_imgs := $(patsubst %.dia, %.png, $(wildcard $(path_doc_img)/*.dia)) doc_imgs_txt := $(patsubst %.dia, %.png, $(wildcard $(path_doc_img)/*.txt)) diff --git a/lib/class_builder.js b/lib/class_builder.js index c19ed0f..5a63dc1 100644 --- a/lib/class_builder.js +++ b/lib/class_builder.js @@ -541,7 +541,7 @@ function buildMembers( var dest = ( keywordStatic( keywords ) ) ? smethods : members; member_builder.buildGetter( - dest, null, name, value, keywords + dest, null, name, value, keywords, base ); }, @@ -550,7 +550,7 @@ function buildMembers( var dest = ( keywordStatic( keywords ) ) ? smethods : members; member_builder.buildSetter( - dest, null, name, value, keywords + dest, null, name, value, keywords, base ); }, diff --git a/lib/member_builder.js b/lib/member_builder.js index 1f87d7d..7653769 100644 --- a/lib/member_builder.js +++ b/lib/member_builder.js @@ -154,6 +154,14 @@ function validateMethod( keywords, prev_data, value, name ) ); } + // do not allow overriding getters/setters + if ( prev_data && ( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override getter/setter '" + name + "' with method" + ); + } + // search for any previous instances of this member if ( prev ) { @@ -236,6 +244,14 @@ exports.buildProp = function( members, meta, name, value, keywords, base ) ); } + // do not allow overriding getters/setters + if ( prev_data && ( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override getter/setter '" + name + "' with property" + ); + } + // do not permit visibility de-escalation if ( prev && ( prev_data.visibility < getVisibilityValue( keywords ) ) ) { @@ -282,10 +298,14 @@ exports.buildProp = function( members, meta, name, value, keywords, base ) * * @param {Object.} keywords parsed keywords * + * @param {Object=} base optional base object to scan + * * @return {undefined} */ -exports.buildGetter = function( members, meta, name, value, keywords ) +exports.buildGetter = function( members, meta, name, value, keywords, base ) { + validateGetterSetter( members, keywords, name, base ); + Object.defineProperty( getMemberVisibility( members, keywords ), name, @@ -312,10 +332,14 @@ exports.buildGetter = function( members, meta, name, value, keywords ) * * @param {Object.} keywords parsed keywords * + * @param {Object=} base optional base object to scan + * * @return {undefined} */ -exports.buildSetter = function( members, meta, name, value, keywords ) +exports.buildSetter = function( members, meta, name, value, keywords, base ) { + validateGetterSetter( members, keywords, name, base ); + Object.defineProperty( getMemberVisibility( members, keywords ), name, @@ -330,6 +354,43 @@ exports.buildSetter = function( members, meta, name, value, keywords ) }; +/** + * Performs common validations on getters/setters + * + * If a problem is found, an exception will be thrown. + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object.} keywords parsed keywords + * @param {string} name getter/setter name + * @param {Object} base optional base to parse + */ +function validateGetterSetter( members, keywords, name, base ) +{ + var prev_data = scanMembers( members, name, base ), + prev = ( prev_data ) ? prev_data.member : null, + + prev_keywords = ( prev && prev.___$$keywords$$ ) + ? prev.___$$keywords$$ + : {} + ; + + if ( prev ) + { + // To speed up the system we'll simply check for a getter/setter, rather + // than checking separately for methods/properties. This is at the + // expense of more detailed error messages. They'll live. + if ( !( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override method or property '" + name + + "' with getter/setter" + ); + } + } +} + + /** * Returns member prototype to use for the requested visibility * @@ -389,18 +450,20 @@ function scanMembers( members, name, base ) // locate requested member by scanning each level of visibility while ( i-- ) { - if ( member = members[ visibility[ i ] ][ name ] ) + var visobj = members[ visibility[ i ] ]; + + // In order to support getters/setters, we must go off of the + // descriptor. We must also ignore base properties (last argument), such + // as Object.prototype.toString(). However, we must still traverse the + // prototype chain. + if ( member = util.getPropertyDescriptor( visobj, name, true ) ) { - // We need to filter out base properties (such as - // Object.prototype.toString()), but we still need to traverse the - // prototype chain. As such, we cannot use hasOwnProperty(). - if ( member !== Object.prototype[ name ] ) - { - return { - member: member, - visibility: ( ( fallback ) ? 0 : i ), - }; - } + return { + get: member.get, + set: member.set, + member: member.value, + visibility: ( ( fallback ) ? 0 : i ), + }; } } diff --git a/lib/util.js b/lib/util.js index afd599d..60269b7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -419,6 +419,95 @@ exports.arrayShrink = function( items ) }; +/** + * Uses Object.getOwnPropertyDescriptor if available, otherwise provides our own + * implementation to fall back on + * + * If the environment does not support retrieving property descriptors (ES5), + * then the following will be true: + * - get/set will always be undefined + * - writable, enumerable and configurable will always be true + * - value will be the value of the requested property on the given object + * + * @param {Object} obj object to check property on + * @param {string} prop property to retrieve descriptor for + * + * @return {Object} descriptor for requested property or undefined if not found + */ +exports.getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor + || function( obj, prop ) + { + if ( !Object.prototype.hasOwnProperty.call( obj, prop ) ) + { + return undefined; + } + + // fallback response + return { + get: undefined, + set: undefined, + + writable: true, + enumerable: true, + configurable: true, + + value: obj[ prop ], + }; + }; + + +/** + * Travels down the prototype chain of the given object in search of the + * requested property and returns its descriptor + * + * This operates as Object.getOwnPropertyDescriptor(), except that it traverses + * the prototype chain. For environments that do not support __proto__, it will + * not traverse the prototype chain and essentially serve as an alias for + * getOwnPropertyDescriptor(). + * + * This method has the option to ignore the base prototype. This is useful to, + * for example, not catch properties like Object.prototype.toString() when + * searching for 'toString' on an object. + * + * @param {Object} obj object to check property on + * @param {string} prop property to retrieve descriptor for + * @param {bool} nobase whether to ignore the base prototype + * + * @return {Object} descriptor for requested property or undefined if not found + */ +exports.getPropertyDescriptor = function( obj, prop, nobase ) +{ + // false by default + nobase = !!nobase; + + // note that this uses util's function, not Object's + var desc = exports.getOwnPropertyDescriptor( obj, prop ), + next = obj.__proto__; + + // if we didn't find a descriptor and a prototype is available, recurse down + // the prototype chain, ensuring that the next prototype has a prototype if + // the base is to be excluded + if ( !desc && next && ( !nobase || next.__proto__ ) ) + { + return exports.getPropertyDescriptor( obj.__proto__, prop, nobase ); + } + + // return the descriptor or undefined if no prototype is available + return desc; +}; + + +/** + * Indicates whether or not the getPropertyDescriptor method is capable of + * traversing the prototype chain + * + * @type {boolean} + */ +exports.defineSecureProp( exports.getPropertyDescriptor, 'canTraverse', + ( {}.__proto__ ) ? true : false +); + + /** * Appropriately returns defineSecureProp implementation to avoid check on each * invocation diff --git a/lib/warn.js b/lib/warn.js new file mode 100644 index 0000000..af45948 --- /dev/null +++ b/lib/warn.js @@ -0,0 +1,168 @@ +/** + * ease.js warning system + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package core + */ + +/** + * Active warning handler + * @type {function()} + */ +var _handler = null; + + +/** + * Permits wrapping an exception as a warning + * + * Warnings are handled differently by the system, depending on the warning + * level that has been set. + * + * @param {Error} e exception (error) to wrap + * + * @return {Warning} new warning instance + */ +var Warning = exports.Warning = function( e ) +{ + // allow instantiation without use of 'new' keyword + if ( !( this instanceof Warning ) ) + { + return new Warning( e ); + } + + // ensure we're wrapping an exception + if ( !( e instanceof Error ) ) + { + throw TypeError( "Must provide exception to wrap" ); + } + + // copy over the message for convenience + this.message = e.message; + this._error = e; +}; + +Warning.prototype = Error(); + + +/** + * Return the error wrapped by the warning + * + * @return {Error} wrapped error + */ +Warning.prototype.getError = function() +{ + return this._error; +}; + + +/** + * Core warning handlers + * @type {Object} + */ +exports.handlers = { + /** + * Logs message to console + * + * Will attempt to log using console.warn(), falling back to console.log() + * if necessary and aborting entirely if neither is available. + * + * This is useful as a default option to bring problems to the developer's + * attention without affecting the control flow of the software. + * + * @param {Warning} warning to log + * + * @return {undefined} + */ + log: function( warning ) + { + var dest; + + console && ( dest = console.warn || console.log ) && + dest( warning.message ); + }, + + + /** + * Throws the error associated with the warning + * + * This handler is useful for development and will ensure that problems are + * brought to the attention of the developer. + * + * @param {Warning} warning to log + * + * @return {undefined} + */ + throwError: function( warning ) + { + throw warning.getError(); + }, + + + /** + * Ignores warnings + * + * This is useful in a production environment where (a) warnings will affect + * the reputation of the software or (b) warnings may provide too much + * insight into the software. If using this option, you should always + * develop in a separate environment so that the system may bring warnings + * to your attention. + * + * @param {Warning} warning to log + * + * @return {undefined} + */ + dismiss: function( warning ) + { + // do nothing + }, +}; + + +/** + * Sets the active warning handler + * + * You may use any of the predefined warning handlers or pass your own function. + * + * @param {function( Warning )} handler warning handler + * + * @return {undefined} + */ +exports.setHandler = function( handler ) +{ + _handler = handler; +}; + + +/** + * Handles a warning using the active warning handler + * + * @param {Warning} warning warning to handle + * + * @return {undefined} + */ +exports.handle = function( warning ) +{ + _handler( warning ); +} + + +// set the default handler +_handler = exports.handlers.log; + diff --git a/test/test-member_builder-gettersetter.js b/test/test-member_builder-gettersetter.js index 6a3ece8..d17d461 100644 --- a/test/test-member_builder-gettersetter.js +++ b/test/test-member_builder-gettersetter.js @@ -56,15 +56,41 @@ function setUp() /** * Partially applied function to quickly build getter from common test data */ -function buildGetterSetterQuick( keywords, val ) +function buildGetterSetterQuick( keywords, val, preserve_prior, use ) { + preserve_prior = !!preserve_prior; + use = ( use === undefined ) ? 0 : +use; + keywords = keywords || {}; val = val || value; - setUp(); + if ( !preserve_prior ) + { + setUp(); + } - buildGetter( members, meta, name, val, keywords ); - buildSetter( members, meta, name, val, keywords ); + if ( use == 0 || use == 1 ) + { + buildGetter( members, meta, name, val, keywords ); + } + if ( use == 0 || use == 2 ) + { + buildSetter( members, meta, name, val, keywords ); + } +} + + +function testEach( test ) +{ + test( 'getter', function( keywords, val, preserve ) + { + buildGetterSetterQuick.call( this, keywords, val, preserve, 1 ); + } ); + + test( 'setter', function( keywords, val, preserve ) + { + buildGetterSetterQuick.call( this, keywords, val, preserve, 2 ); + } ); } @@ -176,3 +202,62 @@ function assertOnlyVisibility( vis, name, value, message ) } )(); + +/** + * Getters/setters should not be able to override methods, for the obvious + * reason that they are two different types and operate entirely differently. Go + * figure. + */ +testEach( function testCannotOverrideMethodWithGetterOrSetter( type, build ) +{ + setUp(); + + // method + members[ 'public' ][ name ] = function() {}; + + try + { + // attempt to override method with getter/setter (should fail) + build( { 'public': true }, null, true ); + } + catch ( e ) + { + assert.ok( e.message.search( name ) !== -1, + "Method override error message should contain getter/setter name" + ); + return; + } + + assert.fail( type + " should not be able to override methods"); +} ); + + +/** + * Getters/setters should not be able to override properties. While, at first, + * this concept may seem odd, keep in mind that the parent would likely not + * expect a subtype to be able to override property assignments. This could open + * up holes to exploit the parent class. + */ +testEach( function testCannotOverridePropertiesWithGetterOrSetter( type, build ) +{ + setUp(); + + // declare a property + members[ 'public' ][ name ] = 'foo'; + + try + { + // attempt to override property with getter/setter (should fail) + build( { 'public': true }, null, true ); + } + catch ( e ) + { + assert.ok( e.message.search( name ) !== -1, + "Property override error message should contain getter/setter name" + ); + return; + } + + assert.fail( type + " should not be able to override properties" ); +} ); + diff --git a/test/test-member_builder-method.js b/test/test-member_builder-method.js index 30a6f54..24d39b2 100644 --- a/test/test-member_builder-method.js +++ b/test/test-member_builder-method.js @@ -25,7 +25,8 @@ var common = require( './common' ), assert = require( 'assert' ), mb_common = require( __dirname + '/inc-member_builder-common' ), - builder = common.require( 'member_builder' ) + builder = common.require( 'member_builder' ), + util = common.require( 'util' ) ; mb_common.funcVal = 'foobar'; @@ -440,3 +441,85 @@ mb_common.assertCommon(); }, TypeError, "Cannot declare private abstract method" ); } )(); + +/** + * While getters are technically methods, it doesn't make sense to override + * getters/setters with methods because they are fundamentally different. + */ +( function testCannotOverrideGetters() +{ + if ( util.definePropertyFallback() ) + { + return; + } + + mb_common.members[ 'public' ] = {}; + Object.defineProperty( mb_common.members[ 'public' ], mb_common.name, { + get: function() {}, + } ); + + try + { + mb_common.value = function() {}; + mb_common.buildMemberQuick( {}, true ); + } + catch ( e ) + { + assert.ok( e.message.search( mb_common.name ) !== -1, + "Method override getter failure should contain method name" + ); + + // ensure we have the correct error + assert.ok( e.message.search( 'getter' ) !== -1, + "Proper error is thrown for getter override failure" + ); + + return; + } + + assert.fail( + "Should not be permitted to override getters with methods" + ); +} )(); + + +/** + * While setters are technically methods, it doesn't make sense to override + * getters/setters with methods because they are fundamentally different. + */ +( function testCannotOverrideSetters() +{ + if ( util.definePropertyFallback() ) + { + return; + } + + mb_common.members[ 'public' ] = {}; + Object.defineProperty( mb_common.members[ 'public' ], mb_common.name, { + set: function() {}, + } ); + + try + { + mb_common.value = function() {}; + mb_common.buildMemberQuick( {}, true ); + } + catch ( e ) + { + assert.ok( e.message.search( mb_common.name ) !== -1, + "Method override setter failure should contain method name" + ); + + // ensure we have the correct error + assert.ok( e.message.search( 'setter' ) !== -1, + "Proper error is thrown for setter override failure" + ); + + return; + } + + assert.fail( + "Should not be permitted to override setters with methods" + ); +} )(); + diff --git a/test/test-member_builder-prop.js b/test/test-member_builder-prop.js index eddbfda..a2aaf66 100644 --- a/test/test-member_builder-prop.js +++ b/test/test-member_builder-prop.js @@ -25,7 +25,8 @@ var common = require( './common' ), assert = require( 'assert' ), mb_common = require( __dirname + '/inc-member_builder-common' ), - builder = common.require( 'member_builder' ) + builder = common.require( 'member_builder' ), + util = common.require( 'util' ) ; @@ -92,3 +93,85 @@ mb_common.assertCommon(); assert.fail( "Should not be permitted to declare virtual properties" ); } )(); + +/* + * While getters act as properties, it doesn't make sense to override + * getters/setters with properties because they are fundamentally different. + */ +( function testCannotOverrideGetters() +{ + if ( util.definePropertyFallback() ) + { + return; + } + + mb_common.members[ 'public' ] = {}; + Object.defineProperty( mb_common.members[ 'public' ], mb_common.name, { + get: function() {}, + } ); + + try + { + mb_common.value = 'foo'; + mb_common.buildMemberQuick( {}, true ); + } + catch ( e ) + { + assert.ok( e.message.search( mb_common.name ) !== -1, + "Property override getter failure should contain property name" + ); + + // ensure we have the correct error + assert.ok( e.message.search( 'getter' ) !== -1, + "Proper error is thrown for getter override failure" + ); + + return; + } + + assert.fail( + "Should not be permitted to override getters with properties" + ); +} )(); + + +/** + * While setters act as properties, it doesn't make sense to override + * getters/setters with properties because they are fundamentally different. + */ +( function testCannotOverrideSetters() +{ + if ( util.definePropertyFallback() ) + { + return; + } + + mb_common.members[ 'public' ] = {}; + Object.defineProperty( mb_common.members[ 'public' ], mb_common.name, { + set: function() {}, + } ); + + try + { + mb_common.value = 'foo'; + mb_common.buildMemberQuick( {}, true ); + } + catch ( e ) + { + assert.ok( e.message.search( mb_common.name ) !== -1, + "Property override setter failure should contain method name" + ); + + // ensure we have the correct error + assert.ok( e.message.search( 'setter' ) !== -1, + "Proper error is thrown for setter override failure" + ); + + return; + } + + assert.fail( + "Should not be permitted to override setters with properties" + ); +} )(); + diff --git a/test/test-util-get-property-descriptor.js b/test/test-util-get-property-descriptor.js new file mode 100644 index 0000000..c33d914 --- /dev/null +++ b/test/test-util-get-property-descriptor.js @@ -0,0 +1,137 @@ +/** + * Tests util.getPropertyDescriptor + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package test + */ + +var common = require( './common' ), + assert = require( 'assert' ), + util = common.require( 'util' ), + + get_set = !( util.definePropertyFallback() ) +; + + +/** + * If Object.getOwnPropertyDescriptor is provided by our environment, it should + * be used by util + */ +( function testUtilGetOwnPropertyDescriptorIsObjectsIfAvailable() +{ + if ( Object.getOwnPropertyDescriptor ) + { + assert.strictEqual( + util.getOwnPropertyDescriptor, + Object.getOwnPropertyDescriptor, + "Util should use Object.getOwnPropertyDescriptor if available" + ); + } +} )(); + + +/** + * The function should provide a boolean value indicating whether it can + * traverse the prototype chain + */ +( function testIndicatesWhetherTraversalIsPossible() +{ + var traversable = ( {}.__proto__ ) ? true : false; + + assert.equal( util.getPropertyDescriptor.canTraverse, traversable, + "Indicates whether traversal is possible" + ); +} )(); + + +/** + * We don't want tricksters to get funky with our system + */ +( function testTraversablePropertyIsNonWritable() +{ + var getDesc; + + if ( get_set ) + { + assert.equal( + Object.getOwnPropertyDescriptor( + util.getPropertyDescriptor, 'canTraverse' + ).writable, + false, + "Should not be able to alter canTravese value" + ); + } +} )(); + + +/** + * The return value should mimic Object.getOwnPropertyDescriptor() if we're not + * having to traverse the prototype chain + */ +( function testActsExactlyAsGetOwnPropertyDescriptorInEs5SystemsOnSameObject() +{ + var obj = { foo: 'bar' }, + desc1 = util.getOwnPropertyDescriptor( obj, 'foo' ), + desc2 = util.getPropertyDescriptor( obj, 'foo' ) + ; + + assert.deepEqual( desc1, desc2, + "When operating one level deep, should return same as " + + "Object.getOwnPropertyDescriptor" + ); +} )(); + + +/** + * If we *do* have to start traversing the prototype chain (which + * Object.getOwnPropertyDescriptor() cannot do), then it should be as if we + * called Object.getOwnPropertyDescriptor() on the object in the prototype chain + * containing the requested property. + */ +( function testTraversesThePrototypeChain() +{ + // if we cannot traverse the prototype chain, this test is pointless + if ( !util.getPropertyDescriptor.canTraverse ) + { + return; + } + + var proto = { foo: 'bar' }, + obj = function() {} + ; + + obj.prototype = proto; + + // to give ourselves the prototype chain (we don't want to set __proto__ + // because this test will also be run on pre-ES5 engines) + var inst = new obj(), + + // get the actual descriptor + expected = util.getOwnPropertyDescriptor( proto, 'foo' ), + + // attempt to gather the descriptor from the prototype chain + given = util.getPropertyDescriptor( inst, 'foo' ) + ; + + assert.deepEqual( given, expected, + "Properly traverses the prototype chain to retrieve the descriptor" + ); +} )(); + diff --git a/test/test-warn-exception.js b/test/test-warn-exception.js new file mode 100644 index 0000000..72cac1a --- /dev/null +++ b/test/test-warn-exception.js @@ -0,0 +1,101 @@ +/** + * Tests the Warning prototype + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package test + */ + +var common = require( './common' ), + assert = require( 'assert' ), + Warning = common.require( 'warn' ).Warning +; + + +/** + * Warning's prototype should be Error to ensure instanceof() checks work + * properly + */ +( function testWarningIsAvailableAndHasErrorAsPrototype() +{ + assert.ok( ( Warning.prototype instanceof Error ), + "Warning should be an instance of Error" + ); +} )(); + + +/** + * Just as with the other Error classes, as well as all ease.js classes, the + * 'new' keyword should be optional when instantiating the class + */ +( function testNewKeywordIsNotRequiredForInstantiation() +{ + assert.ok( Warning( Error( '' ) ) instanceof Warning, + "'new' keyword should not be necessary to instantiate Warning" + ); +} )(); + + +/** + * Warning message should be taken from the exception passed to it + */ +( function testCanWarningMessageIsSetFromWrappedException() +{ + var err = Error( 'oshit' ), + warning = Warning( err ); + + assert.equal( warning.message, err.message, + "Warning message should be taken from wrapped exception" + ); +} )(); + + +/** + * The whole point of Warning is to wrap an exception. So, ensure that one is + * wrapped. + */ +( function testThrowsExceptionIfNoExceptionIsWrapped() +{ + assert.throws( function() + { + Warning( /* nothing provided to wrap */ ); + }, TypeError, "Exception should be thrown if no exception is provided" ); + + assert.throws( function() + { + Warning( 'not an exception' ); + }, TypeError, "Exception should be thrown if given value is not an Error" ); +} )(); + + +/** + * We must provide access to the wrapped exception so that it can be properly + * handled. Warning is only intended to provide additional information so that + * ease.js may handle it differently than other Error instances. + */ +( function testCanRetrieveWrappedException() +{ + var err = Error( 'foo' ), + warning = Warning( err ); + + assert.deepEqual( err, warning.getError(), + "Can retrieve wrapped exception" + ); +} )(); + diff --git a/test/test-warn-handlers.js b/test/test-warn-handlers.js new file mode 100644 index 0000000..6c875b0 --- /dev/null +++ b/test/test-warn-handlers.js @@ -0,0 +1,179 @@ +/** + * Tests core warning handlers + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package test + */ + +var common = require( './common' ), + assert = require( 'assert' ), + warn = common.require( 'warn' ), + Warning = warn.Warning, + + warning = Warning( Error( 'gninraw' ) ) +; + + +/** + * Return the console object, without throwing errors if it does not exist + * + * @return {Object} console + */ +function backupConsole() +{ + // ensure that we don't throw errors if console is not defined + if ( typeof console !== 'undefined' ) + { + return console; + } + + return undefined; +} + + +/** + * The log warning handler should log warnings to the console + */ +( function testLogWarningHandlerLogsMessageToConsole() +{ + var logged = false, + + // back up console ref + console_ = backupConsole() + ; + + // mock console + console = { + warn: function( message ) + { + assert.equal( message, warning.message, + "Should log proper message to console" + ); + + logged = true; + }, + }; + + // call handler with the warning + warn.handlers.log( warning ); + + assert.equal( logged, true, + "Message should be logged to console" + ); + + // restore console + console = console_; +} )(); + + +/** + * Some environments may not have a console reference, or they may not have + * console.warn. In this case, we just want to make sure we don't throw an error + * when attempting to invoke undefined, or access a property of undefined. + */ +( function testLogWarningHandlerHandlesMissingConsole() +{ + // back up console + var console_ = backupConsole(); + + // destroy it + console = undefined; + + // attempt to log + warn.handlers.log( warning ); + + // restore console + console = console_; +} )(); + + +/** + * Furthermore, an environment may implement console.log(), but not + * console.warn(). By default, we use warn(), so let's ensure we can fall back + * to log() if warn() is unavailable. + */ +( function testLogWarningHandlerWillFallBackToLogMethodIfWarnIsMissing() +{ + // back up and overwrite console to contain only log() + var console_ = backupConsole(), + given = ''; + + console = { + log: function( message ) + { + given = message; + } + }; + + // attempt to log + warn.handlers.log( warning ); + + assert.equal( given, warning.message, + "Should fall back to log() and log proper message" + ); + + // restore console + console = console_; +} )(); + + +/** + * The throwError warning handler should throw the wrapped error as an exception + */ +( function testThrowErrorWarningHandlerThrowsWrappedError() +{ + try + { + warn.handlers.throwError( warning ); + } + catch ( e ) + { + assert.deepEqual( e, warning.getError(), + "Wrapped exception should be thrown" + ); + + return; + } + + assert.fail( "Wrapped exception should be thrown" ); +} )(); + + +/** + * The 'dismiss' error handler is a pretty basic concept. Simply do nothing. We + * don't want to log, we don't want to throw anything, we just want to pretend + * nothing ever happened and move on our merry way. This is intended for use in + * production environments where providing warnings may provide too much insight + * into the software. + */ +( function testDismissWarningHandlerShouldDoNothing() +{ + // destroy the console to ensure nothing is logged + var console_ = backupConsole(); + console = undefined; + + // don't catch anything, to ensure no errors occur and that no exceptions + // are thrown + warn.handlers.dismiss( warning ); + + // restore console + console = console_; +} )(); + diff --git a/test/test-warn-impl.js b/test/test-warn-impl.js new file mode 100644 index 0000000..7e714ae --- /dev/null +++ b/test/test-warn-impl.js @@ -0,0 +1,83 @@ +/** + * Tests warning system implementation + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package test + */ + +var common = require( './common' ), + assert = require( 'assert' ), + warn = common.require( 'warn' ) +; + + +/** + * The default warning handler should be the 'log' handler. This is a friendly + * compromise that will allow the developer to be warned of potential issues + * without affecting program execution. + */ +( function testDefaultHandlerIsLogger() +{ + // back up console object + var console_ = ( typeof console !== 'undefined' ) ? console : undefined, + called = false; + + // stub it + console = { + warn: function() + { + called = true; + }, + }; + + warn.handle( warn.Warning( Error( 'foo' ) ) ); + + assert.ok( called, + "Default handler will log to console" + ); + + // restore console + console = console_; +} )(); + + +/** + * The warning handler can be altered at runtime. Ensure we can set it and call + * it appropriately. We do not need to use one of the pre-defined handlers. + */ +( function testCanSetAndCallWarningHandler() +{ + var given, + warning = warn.Warning( Error( 'foo' ) ); + + // set a stub warning handler + warn.setHandler( function( warn ) + { + given = warn; + } ); + + // trigger the handler + warn.handle( warning ); + + assert.deepEqual( given, warning, + "Set warning handler should be called with given Warning" + ); +} )(); + diff --git a/tools/combine b/tools/combine index e968e5b..ed2bf6f 100755 --- a/tools/combine +++ b/tools/combine @@ -29,7 +29,7 @@ TPL_VAR='/**{CONTENT}**/' RMTRAIL="$PATH_TOOLS/rmtrail" # order matters -CAT_MODULES="prop_parser util propobj member_builder class_builder" +CAT_MODULES="warn prop_parser util propobj member_builder class_builder" CAT_MODULES="$CAT_MODULES class class_final class_abstract interface" ## diff --git a/tools/combine-test.tpl b/tools/combine-test.tpl index 3e07c31..08540b9 100644 --- a/tools/combine-test.tpl +++ b/tools/combine-test.tpl @@ -73,7 +73,7 @@ module.assert = { exports: { return; } - if ( cmp instanceof Array ) + if ( ( cmp instanceof Array ) && ( val instanceof Array ) ) { var i = 0, len = cmp.length; @@ -86,7 +86,7 @@ module.assert = { exports: { return; } - else if ( cmp instanceof Object ) + else if ( ( typeof cmp === 'object' ) && ( typeof val === 'object' ) ) { for ( var i in cmp ) {