diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index bb413d0..11bda6b 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -25,8 +25,8 @@ */ var util = require( './util' ), - warn = require( './warn' ), - Warning = warn.Warning, + Warning = require( './warn' ).Warning, + Symbol = require( './util/Symbol' ), hasOwn = Object.prototype.hasOwnProperty, @@ -74,7 +74,18 @@ var util = require( './util' ), '__construct': true, 'toString': true, '__toString': true, - }; + }, + + /** + * Symbol used to encapsulate internal data + * + * Note that this is intentionally generated *outside* the ClassBuilder + * instance; this ensures that it is properly encapsulated and will not + * be exposed on the Classbuilder instance (which would defeat the + * purpose). + */ + _priv = Symbol() +; /** @@ -90,15 +101,23 @@ var util = require( './util' ), * @constructor */ module.exports = exports = -function ClassBuilder( member_builder, visibility_factory ) +function ClassBuilder( warn_handler, member_builder, visibility_factory ) { // allow ommitting the 'new' keyword if ( !( this instanceof exports ) ) { // module.exports for Closure Compiler - return new module.exports( member_builder, visibility_factory ); + return new module.exports( + warn_handler, member_builder, visibility_factory + ); } + /** + * Determines how warnings should be handled + * @type {WarningHandler} + */ + this._warnHandler = warn_handler; + /** * Used for building class members * @type {Object} @@ -214,11 +233,11 @@ exports.getForcedPublicMethods = function() * * @param {Function|Object} cls class from which to retrieve metadata * - * @return {__class_meta} + * @return {__class_meta} or null if unavailable */ exports.getMeta = function( cls ) { - return cls.___$$meta$$ || {}; + return ( cls[ _priv ] || {} ).meta || null; } @@ -319,12 +338,14 @@ exports.prototype.build = function extend( _, __ ) props: this._memberBuilder.initMembers(), }, + meta = exports.getMeta( base ) || {}, + abstract_methods = - util.clone( exports.getMeta( base ).abstractMethods ) + util.clone( meta.abstractMethods ) || { __length: 0 }, virtual_members = - util.clone( exports.getMeta( base ).virtualMembers ) + util.clone( meta.virtualMembers ) || {} ; @@ -333,7 +354,7 @@ exports.prototype.build = function extend( _, __ ) { throw Error( "Cannot extend final class " + - ( base.___$$meta$$.name || '(anonymous)' ) + ( base[ _priv ].meta.name || '(anonymous)' ) ); } @@ -366,7 +387,7 @@ exports.prototype.build = function extend( _, __ ) // properties initialized by the ctor are implicitly public; otherwise, // proxying will fail to take place // TODO: see Class.isA TODO - if ( prototype.___$$vis$$ === undefined ) + if ( ( prototype[ _priv ] || {} ).vis === undefined ) { this._discoverProtoProps( prototype, prop_init ); } @@ -396,7 +417,7 @@ exports.prototype.build = function extend( _, __ ) // intercept warnings /only/ if ( e instanceof Warning ) { - warn.handle( e ); + this._warnHandler.handle( e ); } else { @@ -790,16 +811,19 @@ function validateAbstract( ctor, cname, abstract_methods, auto ) */ exports.prototype.createCtor = function( cname, abstract_methods, members ) { - // concrete class + var new_class; + if ( abstract_methods.__length === 0 ) { - return this.createConcreteCtor( cname, members ); + new_class = this.createConcreteCtor( cname, members ); } - // abstract class else { - return this.createAbstractCtor( cname ); + new_class = this.createAbstractCtor( cname ); } + + util.defineSecureProp( new_class, _priv, {} ); + return new_class; } @@ -858,7 +882,9 @@ exports.prototype.createConcreteCtor = function( cname, members ) // handle internal trait initialization logic, if provided if ( typeof this.___$$tctor$$ === 'function' ) { - this.___$$tctor$$.call( this ); + // FIXME: we're exposing _priv to something that can be + // malicously set by the user; encapsulate tctor + this.___$$tctor$$.call( this, _priv ); } // call the constructor, if one was provided @@ -982,7 +1008,8 @@ exports.prototype._attachPropInit = function( inherit = !!inherit; var iid = this.__iid, - parent = prototype.___$$parent$$; + parent = prototype.___$$parent$$, + vis = this[ _priv ].vis; // first initialize the parent's properties, so that ours will overwrite // them @@ -998,7 +1025,7 @@ exports.prototype._attachPropInit = function( // this will return our property proxy, if supported by our environment, // otherwise just a normal object with everything merged in var inst_props = _self._visFactory.createPropProxy( - this, this.___$$vis$$, properties[ 'public' ] + this, vis, properties[ 'public' ] ); // Copies all public and protected members into inst_props and stores @@ -1006,7 +1033,7 @@ exports.prototype._attachPropInit = function( // chain and is returned. This is stored in a property referenced by the // class id, so that the private members can be swapped on each method // request, depending on calling context. - var vis = this.___$$vis$$[ cid ] = _self._visFactory.setup( + var vis = vis[ cid ] = _self._visFactory.setup( inst_props, properties, members ); @@ -1230,21 +1257,13 @@ function createMeta( func, cparent ) // copy the parent prototype's metadata if it exists (inherit metadata) if ( parent_meta ) { - func.___$$meta$$ = util.clone( parent_meta, true ); - } - else - { - // create empty - func.___$$meta$$ = { - implemented: [], - }; + return func[ _priv ].meta = util.clone( parent_meta, true ); } - // store the metadata in the prototype as well (inconsiderable overhead; - // it's just a reference) - func.prototype.___$$meta$$ = func.___$$meta$$; - - return func.___$$meta$$; + // create empty + return func[ _priv ].meta = { + implemented: [], + }; } @@ -1295,8 +1314,12 @@ function initInstance( instance ) var prot = function() {}; prot.prototype = instance; + // initialize our *own* private metadata store; do not use the + // prototype's + util.defineSecureProp( instance, _priv, {} ); + // add the visibility objects to the data object for this class instance - instance.___$$vis$$ = new prot(); + instance[ _priv ].vis = new prot(); } @@ -1350,9 +1373,10 @@ exports.getMethodInstance = function( inst, cid ) } var iid = inst.__iid, - data = inst.___$$vis$$; + priv = inst[ _priv ], + data; - return ( iid && data ) + return ( iid && priv && ( data = priv.vis ) ) ? data[ cid ] : null ; diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js index 777fcf6..8c394a1 100644 --- a/lib/MemberBuilder.js +++ b/lib/MemberBuilder.js @@ -30,7 +30,6 @@ */ var util = require( './util' ), - Warning = require( './warn' ).Warning, visibility = [ 'public', 'protected', 'private' ] ; diff --git a/lib/Trait.js b/lib/Trait.js index febefb9..4bd17c2 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -673,12 +673,19 @@ function addTraitInst( T, dfn, tc, base ) * This will lazily create the concrete trait class if it does not already * exist, which saves work if the trait is never used. * - * @param {Object} tc trait class list - * @param {Class} base target supertype + * Note that the private symbol used to encapsulate class data must be + * passed to this function to provide us access to implementation details + * that we really shouldn't be messing around with. :) In particular, we + * need access to the protected visibility object, and there is [currently] + * no API for doing so. + * + * @param {Object} tc trait class list + * @param {Class} base target supertype + * @param {Symbol} privsym symbol used as key for encapsulated data * * @return {undefined} */ -function tctor( tc, base ) +function tctor( tc, base, privsym ) { // instantiate all traits and assign the object to their // respective fields @@ -693,11 +700,11 @@ function tctor( tc, base ) // (but not private); in return, we will use its own protected // visibility object to gain access to its protected members...quite // the intimate relationship - this[ f ] = C( base, this.___$$vis$$ ).___$$vis$$; + this[ f ] = C( base, this[ privsym ].vis )[ privsym ].vis; } // if we are a subtype, be sure to initialize our parent's traits - this.__super && this.__super(); + this.__super && this.__super( privsym ); }; @@ -714,9 +721,9 @@ function tctor( tc, base ) */ function createTctor( tc, base ) { - return function() + return function( privsym ) { - return tctor.call( this, tc, base ); + return tctor.call( this, tc, base, privsym ); }; } diff --git a/lib/class.js b/lib/class.js index 9410dfb..d7b3884 100644 --- a/lib/class.js +++ b/lib/class.js @@ -19,16 +19,31 @@ * along with this program. If not, see . */ +/** + * Console to use for logging + * + * This reference allows an alternative console to be used. Must contain + * warn() or log() methods. + * + * TODO: This needs to be moved into a facade, once more refactoring can be + * done; it was moved out of warn during its refactoring. + * + * @type {Object} + */ +var _console = ( typeof console !== 'undefined' ) ? console : undefined; + var util = require( './util' ), ClassBuilder = require( './ClassBuilder' ), - warn = require( './warn' ), - Warning = warn.Warning, + warn = require( './warn' ), + Warning = warn.Warning, + log_handler = warn.LogHandler( _console ), MethodWrapperFactory = require( './MethodWrapperFactory' ), wrappers = require( './MethodWrappers' ).standard, class_builder = ClassBuilder( + log_handler, require( './MemberBuilder' )( MethodWrapperFactory( wrappers.wrapNew ), MethodWrapperFactory( wrappers.wrapOverride ), @@ -36,7 +51,7 @@ var util = require( './util' ), require( './MemberBuilderValidator' )( function( warning ) { - warn.handle( Warning( warning ) ); + log_handler.handle( Warning( warning ) ); } ) ), @@ -154,6 +169,8 @@ var _dummyinst = { constructor: { prototype: {} } }; /** * Determines whether the provided object is a class created through ease.js * + * TODO: delegate to ClassBuilder + * * @param {Object} obj object to test * * @return {boolean} true if class (created through ease.js), otherwise false @@ -167,10 +184,11 @@ module.exports.isClass = function( obj ) return false; } - // TODO: this just checks one of many internal fields; we need something - // more formal (cannot use a strict ClassBase check because it will fail - // when extending prototypes) - return ( ( obj.prototype.___$$vis$$ !== undefined ) + var meta = ClassBuilder.getMeta( obj ); + + // TODO: we're checking a random field on the meta object; do something + // proper + return ( ( ( meta !== null ) && meta.implemented ) || ( obj.prototype instanceof ClassBuilder.ClassBase ) ) ? true : false @@ -182,6 +200,8 @@ module.exports.isClass = function( obj ) * Determines whether the provided object is an instance of a class created * through ease.js * + * TODO: delegate to ClassBuilder + * * @param {Object} obj object to test * * @return {boolean} true if instance of class (created through ease.js), @@ -191,16 +211,8 @@ module.exports.isClassInstance = function( obj ) { obj = obj || _dummyinst; - if ( !obj.constructor || !obj.constructor.prototype ) - { - return false; - } - - // TODO: see isClass TODO - return ( ( obj.constructor.prototype.___$$vis$$ !== undefined ) - || ( obj instanceof ClassBuilder.ClassBase ) ) - ? true - : false; + // if the constructor is a class, then we must be an instance! + return module.exports.isClass( obj.constructor ); }; diff --git a/lib/interface.js b/lib/interface.js index e7a7dff..a8225c0 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -492,10 +492,13 @@ function attachInstanceOf( iface ) */ function _isInstanceOf( type, instance ) { + // we are interested in the class's metadata, not the instance's + var proto = instance.constructor; + // if no metadata are available, then our remaining checks cannot be // performed var meta; - if ( !instance.__cid || !( meta = ClassBuilder.getMeta( instance ) ) ) + if ( !instance.__cid || !( meta = ClassBuilder.getMeta( proto ) ) ) { return false; } diff --git a/lib/util.js b/lib/util.js index 5627118..de59fb5 100644 --- a/lib/util.js +++ b/lib/util.js @@ -52,6 +52,9 @@ var can_define_prop = ( function() } )(); +exports.Global = require( './util/Global' ); + + /** * Freezes an object if freezing is supported * diff --git a/lib/util/Global.js b/lib/util/Global.js new file mode 100644 index 0000000..597d47e --- /dev/null +++ b/lib/util/Global.js @@ -0,0 +1,120 @@ +/** + * Global scope handling + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// retrieve global scope; works with ES5 strict mode +(0,eval)( 'var _the_global=this' ); + +// prototype to allow us to augment the global scope for our own purposes +// without polluting the global scope +function _G() {} +_G.prototype = _the_global; + + +/** + * Provides access to and augmentation of global variables + * + * This provides a static method to consistently provide access to the + * object representing the global scope, regardless of environment. Through + * instantiation, its API permits augmenting a local object whose prototype + * is the global scope, providing alternatives to variables that do not + * exist. + */ +function Global() +{ + // allows omitting `new` keyword, consistent with ease.js style + if ( !( this instanceof Global ) ) + { + return new Global(); + } + + // allows us to extend the global object without actually polluting the + // global scope + this._global = new _G(); +} + + +/** + * Provides consistent access to the global scope through all ECMAScript + * versions, for any root variable name, and works with ES5 strict mode. + * + * As an example, Node.js exposes the variable `root` to represent global + * scope, but browsers expose `window`. Further, ES5 strict mode will provde + * an error when checking whether `typeof SomeGlobalVar === 'undefined'`. + * + * @return {Object} global object + */ +Global.expose = function() +{ + return _the_global; +}; + + +Global.prototype = { + /** + * Provide a value for the provided global variable name if it is not + * defined + * + * A function returning the value to assign to NAME should be provided, + * ensuring that the alternative is never even evaluated unless it is + * needed. + * + * The global scope will not be polluted with this alternative; + * consequently, you must access the value using the `get` method. + * + * @param {string} name global variable name + * @param {function()} f function returning value to assign + * + * @return {Global} self + */ + provideAlt: function( name, f ) + { + if ( typeof this._global[ name ] !== 'undefined' ) + { + return; + } + + this._global[ name ] = f(); + return this; + }, + + + /** + * Retrieve global value or provided alternative + * + * This will take into account values provided via `provideAlt`; if no + * alternative was provided, the request will be deleagated to the + * global variable NAME, which may or may not be undefined. + * + * No error will be thrown if NAME is not globally defined. + * + * @param {string} name global variable name + * + * @return {*} value associated with global variable NAME or + * its provided alternative + */ + get: function( name ) + { + return this._global[ name ]; + }, +}; + +module.exports = Global; + diff --git a/lib/util/Symbol.js b/lib/util/Symbol.js new file mode 100644 index 0000000..2b6aa6d --- /dev/null +++ b/lib/util/Symbol.js @@ -0,0 +1,31 @@ +/** + * Forward-compatible subset of ES6 Symbol + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This is *not* intended to be a complete implementation; it merely + * performs what is needed for ease.js, preferring the benefits of the ES6 + * Symbol implementation while falling back to sane ES5 and ES3 options. + */ + +// to be used if there is no global Symbol available +var FallbackSymbol = require( './symbol/FallbackSymbol' ); + +var _root = require( './Global' ).expose(); +module.exports = _root.Symbol || FallbackSymbol; + diff --git a/lib/util/symbol/FallbackSymbol.js b/lib/util/symbol/FallbackSymbol.js new file mode 100644 index 0000000..a74236a --- /dev/null +++ b/lib/util/symbol/FallbackSymbol.js @@ -0,0 +1,88 @@ +/** + * Forward-compatible subset of ES6 Symbol for pre-ES6 environments + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This is *not* intended to be a complete implementation; it merely + * performs what is needed for ease.js. In particular, this pre-ES6 + * implementation will simply generate a random string to be used as a key; + * the caller is expected to add the key to the destination object as + * non-enumerable, if supported by the environment. + */ + +// ensures that, so long as these methods have not been overwritten by the +// time ease.js is loaded, we will maintain a proper reference +var _random = Math.random, + _floor = Math.floor; + +// prefix used for all generated symbol strings (this string is highly +// unlikely to exist in practice); it will produce a string containing a +// non-printable ASCII character that is *not* the null byte +var _root = ' ' + String.fromCharCode( + _floor( _random() * 10 ) % 31 + 1 +) + '$'; + + +/** + * Generate a pseudo-random string (with a common prefix) to be used as an + * object key + * + * The returned key is unique so long as Math.{random,floor} are reliable. + * This will be true so long as (1) the runtime provides a reliable + * implementation and (2) Math.{floor,random} have not been overwritten at + * the time that this module is loaded. This module stores an internal + * reference to this methods, so malicious code loaded after this module + * will not be able to compromise the return value. + * + * Note that the returned string is not wholly random: a common prefix is + * used to ensure that collisions with other keys on objects is highly + * unlikely; you should not rely on this behavior, though, as it is an + * implementation detail that may change in the future. + * + * @return {string} pseudo-random string with common prefix + */ +function FallbackSymbol() +{ + if ( !( this instanceof FallbackSymbol ) ) + { + return new FallbackSymbol(); + } + + this.___$$id$$ = ( _root + _floor( _random() * 1e8 ) ); +} + + +FallbackSymbol.prototype = { + /** + * Return random identifier + * + * This is convenient, as it allows us to both treat the symbol as an + * object of type FallbackSymbol and use the symbol as a key (since + * doing so will automatically call this method). + * + * @return {string} random identifier + */ + toString: function() + { + return this.___$$id$$; + }, +}; + + +module.exports = FallbackSymbol; + diff --git a/lib/warn.js b/lib/warn.js index ddc6bbb..31185f2 100644 --- a/lib/warn.js +++ b/lib/warn.js @@ -1,7 +1,7 @@ /** * ease.js warning system * - * Copyright (C) 2011, 2012, 2013 Free Software Foundation, Inc. + * Copyright (C) 2011, 2012, 2013, 2014 Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -19,184 +19,11 @@ * along with this program. If not, see . */ -/** - * Active warning handler - * @type {?function( Warning )} - */ -var _handler = null; +module.exports = { + Warning: require( './warn/Warning' ), -/** - * Console to use for logging - * - * This reference allows an alternative console to be used. Must contain warn() - * or log() methods. - * - * @type {Object} - */ -var _console = ( typeof console !== 'undefined' ) ? console : undefined; - - -/** - * 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 - * - * @constructor - */ -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" ); - } - - Error.prototype.constructor.call( this, e.message ); - - // copy over the message for convenience - this.message = e.message; - this.name = 'Warning'; - this._error = e; - - this.stack = e.stack && - e.stack.replace( /^.*?\n+/, - this.name + ': ' + this.message + "\n" - ); + DismissiveHandler: require( './warn/DismissiveHandler' ), + LogHandler: require( './warn/LogHandler' ), + ThrowHandler: require( './warn/ThrowHandler' ), }; -// ensures the closest compatibility...just be careful not to modify Warning's -// prototype -Warning.prototype = Error(); -Warning.prototype.constructor = Warning; -Warning.prototype.name = 'Warning'; - - -/** - * 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.call( _console, ( 'Warning: ' + 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 ); -} - - -/** - * Sets active console - * - * @param {Object} console containing warn() or log() method - * - * @return {undefined} - */ -exports.setConsole = function( console ) -{ - _console = console; -}; - - -// set the default handler -_handler = exports.handlers.log; - diff --git a/lib/warn/DismissiveHandler.js b/lib/warn/DismissiveHandler.js new file mode 100644 index 0000000..c54a64d --- /dev/null +++ b/lib/warn/DismissiveHandler.js @@ -0,0 +1,51 @@ +/** + * Dismissive warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * Warning handler that dismisses (ignores) all warnings + * + * This is useful in a production environment. + */ +function DismissiveHandler() +{ + if ( !( this instanceof DismissiveHandler ) ) + { + return new DismissiveHandler(); + } +} + + +DismissiveHandler.prototype = { + /** + * Handle a warning + * + * @param {Warning} warning warning to handle + * @return {undefined} + */ + handle: function( warning ) + { + // intentionally do nothing + }, +} + +module.exports = DismissiveHandler; + diff --git a/lib/warn/LogHandler.js b/lib/warn/LogHandler.js new file mode 100644 index 0000000..9934b50 --- /dev/null +++ b/lib/warn/LogHandler.js @@ -0,0 +1,64 @@ +/** + * Logging warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * Warning handler that logs all warnings to a console + * + * @param {Object} console console with a warn or log method + */ +function LogHandler( console ) +{ + if ( !( this instanceof LogHandler ) ) + { + return new LogHandler( console ); + } + + this._console = console || {}; +} + + +LogHandler.prototype = { + /** + * Handle a warning + * + * 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 warning to handle + * @return {undefined} + */ + handle: function( warning ) + { + var dest = this._console.warn || this._console.log; + dest && dest.call( this._console, + 'Warning: ' + warning.message + ); + }, +} + +module.exports = LogHandler; + diff --git a/lib/warn/ThrowHandler.js b/lib/warn/ThrowHandler.js new file mode 100644 index 0000000..9ac8c79 --- /dev/null +++ b/lib/warn/ThrowHandler.js @@ -0,0 +1,54 @@ +/** + * Throwing warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * Warning handler that throws all warnings as exceptions + */ +function ThrowHandler() +{ + if ( !( this instanceof ThrowHandler ) ) + { + return new ThrowHandler(); + } +} + + +ThrowHandler.prototype = { + /** + * Handle a warning + * + * 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 warning to handle + * @return {undefined} + */ + handle: function( warning ) + { + throw warning.getError(); + }, +} + +module.exports = ThrowHandler; + diff --git a/lib/warn/Warning.js b/lib/warn/Warning.js new file mode 100644 index 0000000..521a6f0 --- /dev/null +++ b/lib/warn/Warning.js @@ -0,0 +1,81 @@ +/** + * Warning prototype + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * 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 + * + * @constructor + */ +function Warning( 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" ); + } + + Error.prototype.constructor.call( this, e.message ); + + // copy over the message for convenience + this.message = e.message; + this.name = 'Warning'; + this._error = e; + + this.stack = e.stack && + e.stack.replace( /^.*?\n+/, + this.name + ': ' + this.message + "\n" + ); +}; + +// ensures the closest compatibility...just be careful not to modify Warning's +// prototype +Warning.prototype = Error(); +Warning.prototype.constructor = Warning; +Warning.prototype.name = 'Warning'; + + +/** + * Return the error wrapped by the warning + * + * @return {Error} wrapped error + */ +Warning.prototype.getError = function() +{ + return this._error; +}; + + +module.exports = Warning; + diff --git a/test/ClassBuilder/ConstTest.js b/test/ClassBuilder/ConstTest.js index 5bc5b95..b9881bf 100644 --- a/test/ClassBuilder/ConstTest.js +++ b/test/ClassBuilder/ConstTest.js @@ -23,8 +23,6 @@ require( 'common' ).testCase( { caseSetUp: function() { - // XXX: get rid of this disgusting mess; we're mid-refactor and all - // these dependencies should not be necessary for testing this.Sut = this.require( 'ClassBuilder' ); this.MethodWrapperFactory = this.require( 'MethodWrapperFactory' ); @@ -34,14 +32,17 @@ require( 'common' ).testCase( setUp: function() { + // XXX: get rid of this disgusting mess; we're mid-refactor and all + // these dependencies should not be necessary for testing this.builder = this.Sut( - this.require( '/MemberBuilder' )( + this.require( 'warn' ).DismissiveHandler(), + this.require( 'MemberBuilder' )( this.MethodWrapperFactory( this.wrappers.wrapNew ), this.MethodWrapperFactory( this.wrappers.wrapOverride ), this.MethodWrapperFactory( this.wrappers.wrapProxy ), this.getMock( 'MemberBuilderValidator' ) ), - this.require( '/VisibilityObjectFactoryFactory' ) + this.require( 'VisibilityObjectFactoryFactory' ) .fromEnvironment() ) }, diff --git a/test/ClassBuilder/FinalTest.js b/test/ClassBuilder/FinalTest.js index d171c7c..c2e429e 100644 --- a/test/ClassBuilder/FinalTest.js +++ b/test/ClassBuilder/FinalTest.js @@ -73,6 +73,7 @@ require( 'common' ).testCase( { // XXX: clean up this mess. var builder = this.require( 'ClassBuilder' )( + this.require( 'warn' ).DismissiveHandler(), this.require( 'MemberBuilder' )(), this.require( 'VisibilityObjectFactoryFactory' ) .fromEnvironment() diff --git a/test/ClassBuilder/MemberRestrictionTest.js b/test/ClassBuilder/MemberRestrictionTest.js index eefe9a9..fa5b798 100644 --- a/test/ClassBuilder/MemberRestrictionTest.js +++ b/test/ClassBuilder/MemberRestrictionTest.js @@ -291,6 +291,7 @@ require( 'common' ).testCase( build = this.require( 'MemberBuilder' )(); var sut = this.Sut( + this.require( 'warn' ).DismissiveHandler(), build, this.require( 'VisibilityObjectFactoryFactory' ) .fromEnvironment() diff --git a/test/ClassBuilder/StaticTest.js b/test/ClassBuilder/StaticTest.js index f2f6a5f..063dabb 100644 --- a/test/ClassBuilder/StaticTest.js +++ b/test/ClassBuilder/StaticTest.js @@ -26,8 +26,6 @@ require( 'common' ).testCase( { this.fallback = this.require( 'util' ).definePropertyFallback(); - // XXX: get rid of this disgusting mess; we're mid-refactor and all - // these dependencies should not be necessary for testing this.ClassBuilder = this.require( 'ClassBuilder' ); this.MemberBuilder = this.require( 'MemberBuilder' ); this.MethodWrapperFactory = this.require( 'MethodWrapperFactory' ); @@ -38,7 +36,10 @@ require( 'common' ).testCase( setUp: function() { + // XXX: get rid of this disgusting mess; we're mid-refactor and all + // these dependencies should not be necessary for testing this.builder = this.ClassBuilder( + this.require( 'warn' ).DismissiveHandler(), this.MemberBuilder( this.MethodWrapperFactory( this.wrappers.wrapNew ), this.MethodWrapperFactory( this.wrappers.wrapOverride ), diff --git a/test/ClassBuilder/VisibilityTest.js b/test/ClassBuilder/VisibilityTest.js index d84706f..b98102e 100644 --- a/test/ClassBuilder/VisibilityTest.js +++ b/test/ClassBuilder/VisibilityTest.js @@ -25,8 +25,6 @@ require( 'common' ).testCase( { caseSetUp: function() { - // XXX: get rid of this disgusting mess; we're mid-refactor and all - // these dependencies should not be necessary for testing this.Sut = this.require( 'ClassBuilder' ); this.MethodWrapperFactory = this.require( 'MethodWrapperFactory' ); @@ -37,7 +35,10 @@ require( 'common' ).testCase( setUp: function() { + // XXX: get rid of this disgusting mess; we're mid-refactor and all + // these dependencies should not be necessary for testing this.builder = this.Sut( + this.require( 'warn' ).DismissiveHandler(), this.require( '/MemberBuilder' )( this.MethodWrapperFactory( this.wrappers.wrapNew ), this.MethodWrapperFactory( this.wrappers.wrapOverride ), diff --git a/test/Util/GlobalTest.js b/test/Util/GlobalTest.js new file mode 100644 index 0000000..1b350ef --- /dev/null +++ b/test/Util/GlobalTest.js @@ -0,0 +1,143 @@ +/** + * Tests global scope handling + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var _global = this; + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'util/Global' ); + this.gobj = this.Sut.expose(); + this.uniq = '___$$easejs$globaltest$$'; + }, + + + /** + * Check common environments and ensure that the returned object is + * strictly equal to the global object for that environment. For + * environments that we do *not* know about, just check for a common + * object that must exist in ES3 and above. + */ + 'Global object represents environment global object': function() + { + switch ( true ) + { + // browser + case _global.window: + this.assertStrictEqual( this.gobj, _global.window ); + break; + + // Node.js + case _global.root: + this.assertStrictEqual( this.gobj, _global.root ); + break; + + // something else; we'll just check for something that should + // exist in >=ES3 + default: + this.assertStrictEqual( this.gobj.Array, Array ); + } + }, + + + /** + * Since ease.js makes use of ECMAScript features when they are + * available, it must also find a way to gracefully degrade to support + * less fortunate environments; the ability to define alternative + * definitions is key to that. + */ + 'Providing alternative will set value if name does not exist': + function() + { + var sut = this.Sut(); + + var field = this.uniq, + value = { _: 'easejsOK' }; + + sut.provideAlt( field, function() { return value; } ); + this.assertStrictEqual( sut.get( field ), value ); + }, + + + /** + * It is also important that our own definitions do not pollute the + * global scope; reasons for this are not just to be polite, but also + * because other code/libraries may provide their own definitions that + * we would not want to interfere with. (Indeed, we'd also want to use + * those definitions, if they already exist before provideAlt is + * called.) + */ + 'Providing alternative will not pollute the global scope': function() + { + this.Sut().provideAlt( this.uniq, function() { return {} } ); + this.assertEqual( this.gobj[ this.uniq ], undefined ); + }, + + + /** + * Our alternatives are unneeded if the object we are providing an + * alternative for is already defined. + */ + 'Providing alternative will not modify global if name exists': + function() + { + var sut = this.Sut(); + + // a field that must exist in ES3+ + var field = 'Array', + orig = this.gobj[ field ]; + + sut.provideAlt( field, function() { return {}; } ); + this.assertStrictEqual( sut.get( field ), orig ); + }, + + + /** + * Once an alternative is defined, it shall be treated as though the + * value were defined globally; providing additional alternatives should + * therefore have no effect. + */ + 'Providing alternative twice will not modify first alternative': + function() + { + var sut = this.Sut(); + field = this.uniq, + expected = { _: 'easejsOK' }; + + // first should provide alternative, second should do nothing + sut.provideAlt( field, function() { return expected; } ); + sut.provideAlt( field, function() { return 'oops'; } ); + + this.assertStrictEqual( sut.get( field ), expected ); + }, + + + 'provideAlt returns self for method chaining': function() + { + var sut = this.Sut(); + + this.assertStrictEqual( sut, + sut.provideAlt( 'foo', function() {} ) + ); + }, +} ); + diff --git a/test/Util/IndexTest.js b/test/Util/IndexTest.js new file mode 100644 index 0000000..d719a56 --- /dev/null +++ b/test/Util/IndexTest.js @@ -0,0 +1,41 @@ +/** + * Tests utility module entry point + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * N.B. Despite this saying that it tests the index (i.e. entry point), this + * is not yet the case; it will be in the future, though. + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'util' ); + }, + + + 'Exposes Global prototype': function() + { + this.assertStrictEqual( + this.Sut.Global, + this.require( 'util/Global' ) + ); + }, +} ); + diff --git a/test/Util/SymbolTest.js b/test/Util/SymbolTest.js new file mode 100644 index 0000000..3748240 --- /dev/null +++ b/test/Util/SymbolTest.js @@ -0,0 +1,43 @@ +/** + * Tests symbol subset + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * N.B. Despite this saying that it tests the index (i.e. entry point), this + * is not yet the case; it will be in the future, though. + */ + + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'util/Symbol' ); + }, + + + /** + * We don't care about the details of this; just make sure that we fail + * in an environment that seems to confuse us. + */ + 'Exports a function': function() + { + this.assertOk( typeof this.Sut === 'function' ); + }, +} ); + diff --git a/test/Util/symbol/FallbackSymbolTest.js b/test/Util/symbol/FallbackSymbolTest.js new file mode 100644 index 0000000..21fb236 --- /dev/null +++ b/test/Util/symbol/FallbackSymbolTest.js @@ -0,0 +1,76 @@ +/** + * Tests pre-ES6 fallback symbol subset + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'util/symbol/FallbackSymbol' ); + }, + + + /** + * Symbols are used to create an object fields that is accessible only + * to the holder of a reference to the symbol used to create that field. + * Since this fallback is intended to be used in environments that do + * not support symbols, the alternative is to return a random string + * that is highly unlikely to exist in practice. However, we must also + * return an object to allow for instanceof checks. See below test for + * more details. + */ + 'Constructor returns an instance of Symbol': function() + { + var result = this.Sut(); + this.assertOk( result instanceof this.Sut ); + }, + + + /** + * The generated string should be unique for each call, making it + * unlikely that its value can be guessed. Of course, this relies on the + * assumption that the runtime's PRNG is reliable and that it has not + * been maliciously rewritten. + * + * Note that we don't test the various implementation details, as that + * is intended to be opaque (see SUT source for details). + */ + 'Generated string varies with each call': function() + { + var gen = {}, + i = 32; + + while ( i-- ) + { + var result = this.Sut(); + if ( gen[ result ] ) + { + this.fail( result, '' ); + } + + gen[ result ] = true; + } + + // this prevents the test from being marked as incomplete + this.assertOk( 'passed' ); + }, +} ); + diff --git a/test/WarnHandlersTest.js b/test/WarnHandlersTest.js deleted file mode 100644 index c2753c0..0000000 --- a/test/WarnHandlersTest.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Tests core warning handlers - * - * Copyright (C) 2014 Free Software Foundation, Inc. - * - * This file is part of GNU ease.js. - * - * ease.js is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -try { void console } catch ( e ) { console = undefined; } - -require( 'common' ).testCase( -{ - caseSetUp: function() - { - // XXX: this has global state - this.Sut = this.require( 'warn' ); - }, - - - setUp: function() - { - this.stubwarn = this.Sut.Warning( Error( 'gninraw' ) ); - }, - - - /** - * The log warning handler should log warnings to the console - */ - '`log\' warning handler logs messages to console': function() - { - var _self = this, - logged = false; - - // mock console - this.Sut.setConsole( { - warn: function( message ) - { - // should prefix with `Warning: ' - _self.assertEqual( - ( 'Warning: ' + _self.stubwarn.message ), - message - ); - - logged = true; - }, - } ); - - // call handler with the warning - this.Sut.handlers.log( this.stubwarn ); - - this.assertOk( logged, true, - "Message should be logged to console" - ); - - // restore console (TODO: will not be necessary once global state is - // removed) - this.Sut.setConsole( 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. - */ - '`log\' warning handler handles missing console': function() - { - var Sut = this.Sut; - - // destroy it - Sut.setConsole( undefined ); - - // attempt to log - var _self = this; - this.assertDoesNotThrow( function() - { - Sut.handlers.log( _self.warnstub ); - } ); - - // restore console - Sut.setConsole( 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. - */ - '`log\' warning handler falls back to log if warn is missing': - function() - { - var given = ''; - - this.Sut.setConsole( { - log: function( message ) - { - given = message; - } - } ); - - // attempt to log - this.Sut.handlers.log( this.stubwarn ); - - this.assertEqual( ( 'Warning: ' + this.stubwarn.message ), given, - "Should fall back to log() and log proper message" - ); - - // restore console - this.Sut.setConsole( console ); - }, - - - /** - * The throwError warning handler should throw the wrapped error as an - * exception - */ - '`throwError\' warning handler throws wrapped error': function() - { - try - { - this.Sut.handlers.throwError( this.stubwarn ); - } - catch ( e ) - { - this.assertStrictEqual( e, this.stubwarn.getError(), - "Wrapped exception should be thrown" - ); - - return; - } - - this.assertFail( "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 such - * warnings are expected to already have been worked out and would only - * confuse/concern the user. - */ - '`dismiss\' warning handler does nothing': function() - { - var Sut = this.Sut; - - // destroy the console to ensure nothing is logged - Sut.setConsole( undefined ); - - // no errors should occur because it should not do anything. - var _self = this; - this.assertDoesNotThrow( function() - { - Sut.handlers.dismiss( _self.warnstub ); - } ); - - // restore console - Sut.setConsole( console ); - }, -} ); diff --git a/test/WarnTest.js b/test/WarnTest.js deleted file mode 100644 index 7206155..0000000 --- a/test/WarnTest.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Tests warning system implementation - * - * Copyright (C) 2014 Free Software Foundation, Inc. - * - * This file is part of GNU ease.js. - * - * ease.js is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -try { void console } catch ( e ) { console = undefined; } - -require( 'common' ).testCase( -{ - setUp: function() - { - // XXX: this uses global state; remove that requirement. - this.Sut = this.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. - */ - 'Default warning handler is `log\'': function() - { - var called = false; - - // stub it - this.Sut.setConsole( { - warn: function() - { - called = true; - }, - } ); - - this.Sut.handle( this.Sut.Warning( Error( 'foo' ) ) ); - this.assertOk( called ); - - // restore console (TODO: this will not be necessary once reliance - // on global state is removed) - this.Sut.setConsole( 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. - */ - 'Can set and call arbitrary warning handler': function() - { - var given, - warning = this.Sut.Warning( Error( 'foo' ) ); - - // set a stub warning handler - this.Sut.setHandler( function( warn ) - { - given = warn; - } ); - - // trigger the handler - this.Sut.handle( warning ); - this.assertDeepEqual( given, warning ); - }, -} ); diff --git a/test/common.js b/test/common.js index 64119dd..845b73b 100644 --- a/test/common.js +++ b/test/common.js @@ -47,6 +47,6 @@ exports.require = function( module ) * * @return {udnefined} */ -exports.testCase = require( __dirname + '/inc-testcase.js' ); +exports.testCase = require( './inc-testcase.js' ); diff --git a/test/warn/DismissiveHandlerTest.js b/test/warn/DismissiveHandlerTest.js new file mode 100644 index 0000000..aef8653 --- /dev/null +++ b/test/warn/DismissiveHandlerTest.js @@ -0,0 +1,57 @@ +/** + * Tests dismissive warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'warn/DismissiveHandler' ); + this.Warning = this.require( 'warn/Warning' ); + }, + + + 'Can be instantiated without `new` keyword': function() + { + this.assertOk( this.Sut() instanceof this.Sut ); + }, + + + /** + * 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 such warnings are expected to already have been worked out and + * would only confuse/concern the user. + * + * Now, testing whether it does anything or not is difficult, since it + * could do, well, anything; that said, we are not passing it anything + * via the ctor, so assuming that it does not rely on or manipulate + * global state, we need only ensure that no exceptions are thrown. + */ + 'Does nothing': function() + { + var _self = this; + this.assertDoesNotThrow( function() + { + _self.Sut().handle( _self.Warning( Error( "Ignore me!" ) ) ); + } ); + }, +} ); diff --git a/test/warn/LogHandlerTest.js b/test/warn/LogHandlerTest.js new file mode 100644 index 0000000..46c84c1 --- /dev/null +++ b/test/warn/LogHandlerTest.js @@ -0,0 +1,126 @@ +/** + * Tests logging warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'warn/LogHandler' ); + this.Warning = this.require( 'warn/Warning' ); + }, + + + setUp: function() + { + this.stubwarn = this.Warning( Error( 'gninraw' ) ); + }, + + + 'Can be instantiated without `new` keyword': function() + { + this.assertOk( this.Sut() instanceof this.Sut ); + }, + + + /** + * Warnings should be logged to the provided console. By default, the + * `warn` method is used (see below tests for fallbacks). + */ + 'Logs messages to console': function() + { + var _self = this, + logged = false; + + // mock console + this.Sut( { + warn: function( message ) + { + // should prefix with `Warning: ' + _self.assertEqual( + ( 'Warning: ' + _self.stubwarn.message ), + message + ); + + logged = true; + }, + } ).handle( this.stubwarn ); + + this.assertOk( logged, true, + "Message should be logged to 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. + */ + 'Ignores missing console': function() + { + var _self = this; + this.assertDoesNotThrow( function() + { + _self.Sut( undefined ).handle( _self.warnstub ); + } ); + }, + + + /** + * 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. + */ + 'Falls back to log if warn is missing': function() + { + var given = ''; + + this.Sut( { + log: function( message ) + { + given = message; + } + } ).handle( this.stubwarn ); + + this.assertEqual( ( 'Warning: ' + this.stubwarn.message ), given, + "Should fall back to log() and log proper message" + ); + }, + + + /** + * If both `console.warn` and `console.log` are defined (which is very + * likely to be the case), the former should take precedence. + */ + '`warn` takes precedence over `log`': function() + { + var log = warn = false; + + this.Sut( { + warn: function() { warn = true }, + log: function() { log = true }, + } ).handle( this.stubwarn ); + + this.assertOk( warn ); + this.assertOk( !log ); + }, +} ); diff --git a/test/warn/ThrowHandlerTest.js b/test/warn/ThrowHandlerTest.js new file mode 100644 index 0000000..d0ce1d5 --- /dev/null +++ b/test/warn/ThrowHandlerTest.js @@ -0,0 +1,60 @@ +/** + * Tests throwing warning handler + * + * Copyright (C) 2014 Free Software Foundation, Inc. + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'warn/ThrowHandler' ); + this.Warning = this.require( 'warn/Warning' ); + }, + + + 'Can be instantiated without `new` keyword': function() + { + this.assertOk( this.Sut() instanceof this.Sut ); + }, + + + /** + * The wrapped error should be thrown as an exception; this effectively + * undoes the warning wrapper. + */ + '`throwError\' warning handler throws wrapped error': function() + { + var warn = this.Warning( Error( 'gninraw' ) ); + + try + { + this.Sut().handle( warn ); + } + catch ( e ) + { + this.assertStrictEqual( e, warn.getError(), + "Wrapped exception should be thrown" + ); + + return; + } + + this.assertFail( "Wrapped exception should be thrown" ); + }, +} ); diff --git a/test/WarningTest.js b/test/warn/WarningTest.js similarity index 100% rename from test/WarningTest.js rename to test/warn/WarningTest.js diff --git a/tools/combine b/tools/combine index 9b804d5..378c393 100755 --- a/tools/combine +++ b/tools/combine @@ -33,8 +33,13 @@ RMTRAIL="$PATH_TOOLS/rmtrail" cat_modules=$( cd "$PATH_TOOLS/../" && grep -rIo ' require(.*)' lib/ \ - | sed "s/^lib\///;s/\.js://;s/require( *'\.\/\(.*\)'.*/\1/" \ - | node tools/combine-order.js + | sed " \ + s/^lib\/\(\(.\+\/\)\?[^/]\+\)\.js: /\1 \2/; + s/require( *'\(.*\)'.*/\1/; + s/[^/\]\+\/\.\.//g; + s/\( \|\/\)\.\//\1/g; + " \ + | node tools/combine-order.js \ ) || { echo "Failed to get module list" >&2 exit 3 @@ -122,6 +127,7 @@ for module in $cat_modules; do echo "( function( module, __dirname )" echo "{" echo " var exports = module.exports = {};" + echo " __cwd = '$( dirname "$module" )';" # add the module, removing trailing commas cat $filename | $RMTRAIL @@ -166,6 +172,7 @@ if [ "$INC_TEST" ]; then echo "( function( module, __dirname )" echo "{" echo " var exports = module.exports = {};" + echo " __cwd = '.';" # write out current test to make debugging easier in browsers with very # little debugging support diff --git a/tools/combine-test.tpl b/tools/combine-test.tpl index 574bb79..7f44107 100644 --- a/tools/combine-test.tpl +++ b/tools/combine-test.tpl @@ -26,7 +26,7 @@ module.common = module['test/common'] = { exports: { testCase: function() { - return require( 'test/inc-testcase' ).apply( this, arguments ); + return require( '/test/inc-testcase' ).apply( this, arguments ); } } }; diff --git a/tools/combine.tpl b/tools/combine.tpl index e97e7e4..16a83f2 100644 --- a/tools/combine.tpl +++ b/tools/combine.tpl @@ -15,7 +15,7 @@ */ var easejs = {}; -( function( ns_exports ) +( function( ns_exports, __cwd ) { /** * CommonJS module exports @@ -42,15 +42,26 @@ var easejs = {}; */ var require = function( module_id ) { - // remove the './' directory prefix (every module is currently included - // via a relative path), stupidly remove ../'s and remove .js extensions - var id_clean = module_id.replace( /^\.?\/|[^/]*?\/\.\.\/|\.js$/, '' ); + // anything that is not an absolute require path will be prefixed + // with __cwd, which is set by the combined module; this allows + // including relative paths (but note that this also means that + // modules that perform ad-hoc conditional requires after another + // module has been processed may not work properly; we don't do + // this, though) + var id_norm = ( module_id.substr( 0, 1 ) === '/' ) + ? module_id + : __cwd + '/' + module_id; + + // strip `../`, poorly strip `./` (for example, it would also strip + // `foo./`, but we know that this won't ever be the case with our + // files), and strip leading `/` + var id_clean = id_norm.replace( /([^\/]+\/\.\.\/|\.\/|^\/)/g, '' ); // attempt to retrieve the module var mod = module[ id_clean ]; if ( mod === undefined ) { - throw "[ease.js] Undefined module: " + module_id; + throw "[ease.js] Undefined module: " + id_clean; } return mod.exports; @@ -65,5 +76,5 @@ var easejs = {}; ns_exports.Interface = module['interface'].exports; ns_exports.Trait = module['Trait'].exports; ns_exports.version = module['version'].exports; -} )( easejs ); +} )( easejs, '.' );