1
0
Fork 0
easejs/test/Trait/DefinitionTest.js

505 lines
16 KiB
JavaScript
Raw Permalink Normal View History

/**
* Tests basic trait definition
*
2015-05-28 01:00:32 -04:00
* Copyright (C) 2014, 2015 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 <http://www.gnu.org/licenses/>.
*/
require( 'common' ).testCase(
{
caseSetUp: function()
{
this.Sut = this.require( 'Trait' );
this.Class = this.require( 'class' );
this.Interface = this.require( 'interface' );
this.AbstractClass = this.require( 'class_abstract' );
2014-03-11 06:58:39 -04:00
this.hasGetSet = !(
this.require( 'util' ).definePropertyFallback()
);
// means of creating anonymous traits
this.ctor = [
this.Sut.extend,
this.Sut,
];
// trait field name conflicts (methods)
this.fconflict = [
[ 'foo', "same name; no keywords",
{ foo: function() {} },
{ foo: function() {} },
],
[ 'foo', "same keywords; same visibility",
{ 'public foo': function() {} },
{ 'public foo': function() {} },
],
// should (at least for the time being) be picked up by existing
// class error checks; TODO: but let's provide trait-specific
// error messages to avoid frustration and infuriation
[ 'foo', "varying keywords; same visibility",
{ 'virtual public foo': function() {} },
{ 'public virtual foo': function() {} },
],
[ 'foo', "different visibility",
{ 'public foo': function() {} },
{ 'protected foo': function() {} },
],
];
this.base = [ this.Class ];
},
/**
* We continue with the same concept used for class
* definitions---extending the Trait module itself will create an
* anonymous trait.
*/
'@each(ctor) Can extend Trait to create anonymous trait': function( T )
{
this.assertOk( this.Sut.isTrait( T( {} ) ) );
},
/**
* A trait can only be used by something else---it does not make sense
* to instantiate them directly, since they form an incomplete picture.
Initial implementation of parameterized traits This is an important feature to permit trait reuse without excessive subtyping---composition over inheritance. For example, consider that you have a `HttpPlainAuth` trait that adds authentication support to some transport layer. Without parameterized traits, you have two options: 1. Expose setters for credentials 2. Trait closure 3. Extend the trait (not yet supported) The first option seems like a simple solution: ```javascript Transport.use( HttpPlainAuth )() .setUser( 'username', 'password' ) .send( ... ); ``` But we are now in the unfortunate situation that our initialization procedure has changed. This, for one, means that authentication logic must be added to anything that instantiates classes that mix in `HttpPlainAuth`. We'll explore that in more detail momentarily. More concerning with this first method is that, not only have we prohibited immutability, but we have also made our code reliant on *invocation order*; `setUser` must be called before `send`. What if we have other traits mixed in that have similar conventions? Normally, this is the type of problem that would be solved with a builder, but would we want every configurable trait to return a new `Transport` instance? All that on top of littering the API---what a mess! The second option is to store configuration data outside of the Trait acting as a closure: ```javascript var _user, _pass; function setCredentials( user, pass ) { _user = user; _pass = pass; } Trait( 'HttpPlainAuth', { /* use _user and _pass */ } ) ``` There are a number of problems with this; the most apparent is that, in this case, the variables `_user` and `_pass` act in place of static fields---all instances will share that data, and if the data is modified, it will affect all instances; you are therefore relying on external state, and mutability is forced upon you. You are also left with an awkward `setCredentials` call that is disjoint from `HttpPlainAuth`. The other notable issue arises if you did want to support instance-specific credentials. You would have to use ease.js' internal identifiers (which is undocumented and subject to change in future versions), and would likely accumulate garbage data as mixin instances are deallocated, since ECMAScript does not have destructor support. To recover from memory leaks, you could instead create a trait generator: ```javascript function createHttpPlainAuth( user, pass ) { return Trait( { /* ... */ } ); } ``` This uses the same closure concept, but generates new traits at runtime. This has various implications depending on your engine, and may thwart future ease.js optimization attempts. The third (which will be supported in the near future) is prohibitive: we'll add many unnecessary traits that are a nightmare to develop and maintain. Parameterized traits are similar in spirit to option three, but without creating new traits each call: traits now support being passed configuration data at the time of mixin that will be passed to every new instance: ```javascript Transport.use( HttpPlainAuth( user, pass ) )() .send( ... ); ``` Notice now how the authentication configuration is isolated to the actual mixin, *prior to* instantiation; the caller performing instantiation need not be aware of this mixin, and so the construction logic can remain wholly generic for all `Transport` types. It further allows for a convenient means of providing useful, reusable exports: ```javascript module.exports = { ServerFooAuth: HttpPlainAuth( userfoo, passfoo ), ServerBarAuth: HttpPlainAuth( userbar, passbar ), ServerFooTransport: Transport.use( module.exports.ServerFooAuth ), // ... }; var module = require( 'foo' ); // dynamic auth Transport.use( foo.ServerFooAuth )().send( ... ); // or predefined classes module.ServerFooTransport().send( ... ); ``` Note that, in all of the above cases, the initialization logic is unchanged---the caller does not need to be aware of any authentication mechanism, nor should the caller care of its existence. So how do you create parameterized traits? You need only define a `__mixin` method: Trait( 'HttpPlainAuth', { __mixin: function( user, pass ) { ... } } ); The method `__mixin` will be invoked upon instantiation of the class into which a particular configuration of `HttpPlainAuth` is mixed into; it was named differently from `__construct` to make clear that (a) traits cannot be instantiated and (b) the constructor cannot be overridden by traits. A configured parameterized trait is said to be an *argument trait*; each argument trait's configuration is discrete, as was demonstrated by `ServerFooAuth` and `ServerBarAuth` above. Once a parameterized trait is configured, its arguments are stored within the argument trait and those arguments are passed, by reference, to `__mixin`. Since any mixed in trait can have its own `__mixin` method, this permits traits to have their own initialization logic without the need for awkward overrides or explicit method calls.
2014-05-28 23:37:36 -04:00
*
* Now, that said, see parameterized traits.
*/
'@each(ctor) Cannot instantiate trait without error': function( T )
{
this.assertThrows( function()
{
T( {} )();
}, Error );
},
/**
* One way that traits acquire meaning is by their use in creating
* classes. This also allows us to observe whether traits are actually
* working as intended without testing too closely to their
* implementation. This test simply ensures that the Class module will
* accept our traits.
*
* Classes consume traits as part of their definition using the `use'
* method. We should be able to then invoke the `extend' method to
* provide our own definition, without having to inherit from another
* class.
*/
'@each(ctor) Base class definition is applied when using traits':
function( T )
{
var expected = 'bar';
var C = this.Class.use( T( {} ) ).extend(
{
foo: expected,
} );
this.assertOk( this.Class.isClass( C ) );
this.assertEqual( C().foo, expected );
},
/**
* Traits contribute to the definition of the class that `use's them;
* therefore, it would stand to reason that we should still be able to
* inherit from a supertype while using traits.
*/
'@each(ctor) Supertype definition is applied when using traits':
function( T )
{
var expected = 'bar',
expected2 = 'baz',
Foo = this.Class( { foo: expected } ),
SubFoo = this.Class.use( T( {} ) )
.extend( Foo, { bar: expected2 } );
var inst = SubFoo();
this.assertOk( this.Class.isA( Foo, inst ) );
this.assertEqual( inst.foo, expected, "Supertype failure" );
this.assertEqual( inst.bar, expected2, "Subtype failure" );
},
/**
* The above tests have ensured that classes are still operable with
* traits; we can now test that traits are mixed into the class
* definition via `use' by asserting on the trait definitions.
*/
'@each(ctor) Trait definition is mixed into base class definition':
function( T )
{
var called = false;
var Trait = T( { foo: function() { called = true; } } ),
inst = this.Class.use( Trait ).extend( {} )();
// if mixin was successful, then we should have the `foo' method.
this.assertDoesNotThrow( function()
{
inst.foo();
}, Error, "Should have access to mixed in fields" );
// if our variable was not set, then it was a bs copy
this.assertOk( called, "Mixed in field copy error" );
},
/**
* The above test should apply just the same to subtypes.
*/
'@each(ctor) Trait definition is mixed into subtype definition':
function( T )
{
var called = false;
var Trait = T( { foo: function() { called = true; } } ),
Foo = this.Class( {} ),
inst = this.Class.use( Trait ).extend( Foo, {} )();
inst.foo();
this.assertOk( called );
},
//
// At this point, we assume that each ctor method is working as expected
// (that is---the same); we will proceed to test only a single method of
// construction under that assumption.
//
/**
* Traits cannot be instantiated, so they need not define __construct
* for themselves; however, they may wish to influence the construction
* of anything that uses them. This is poor practice, since that
* introduces a war between traits to take over the constructor;
* instead, the class using the traits should handle calling the methods
* on the traits and we should disallow traits from attempting to set
* the constructor.
*/
'Traits cannot define __construct': function()
{
try
{
this.Sut( { __construct: function() {} } );
}
catch ( e )
{
this.assertOk( e.message.match( /\b__construct\b/ ) );
return;
}
this.fail( false, true,
"Traits should not be able to define __construct"
);
},
/**
* If two traits attempt to define the same field (by name, regardless
* of its type), then an error should be thrown to warn the developer of
* a problem; automatic resolution would be a fertile source of nasty
* and confusing bugs.
*
* TODO: conflict resolution through aliasing
*/
'@each(fconflict) Cannot mix in multiple concrete methods of same name':
function( dfns )
{
var fname = dfns[ 0 ],
desc = dfns[ 1 ],
A = this.Sut( dfns[ 2 ] ),
B = this.Sut( dfns[ 3 ] );
// this, therefore, should error
try
{
this.Class.use( A, B ).extend( {} );
}
catch ( e )
{
// the assertion should contain the name of the field that
// caused the error
this.assertOk(
e.message.match( '\\b' + fname + '\\b' ),
"Error message missing field name: " + e.message
);
// TODO: we can also make less people hate us if we include the
// names of the conflicting traits; in the case of an anonymous
// trait, maybe include its index in the use list
return;
}
this.fail( false, true, "Mixin must fail on conflict: " + desc );
},
/**
* Traits in ease.js were designed in such a way that an object can be
* considered to be a type of any of the traits that its class mixes in;
* this is consistent with the concept of interfaces and provides a very
* simple and intuitive type system.
*/
'A class is considered to be a type of each used trait': function()
{
var Ta = this.Sut( {} ),
Tb = this.Sut( {} ),
Tc = this.Sut( {} ),
o = this.Class.use( Ta, Tb ).extend( {} )();
// these two were mixed in
this.assertOk( this.Class.isA( Ta, o ) );
this.assertOk( this.Class.isA( Tb, o ) );
// this one was not
this.assertOk( this.Class.isA( Tc, o ) === false );
},
2014-02-10 23:33:07 -05:00
/**
* Ensure that the named class staging object permits mixins.
*/
'Can mix traits into named class': function()
{
var called = false,
T = this.Sut( { foo: function() { called = true; } } );
this.Class( 'Named' ).use( T ).extend( {} )().foo();
this.assertOk( called );
},
/**
* When explicitly defining a class (that is, not mixing into an
* existing class definition), which involves the use of Class or
* AbstractClass, mixins must be terminated with a call to `extend'.
* This allows the system to make a final determination as to whether
* the resulting class is abstract.
*
* Contrast this with Type.use( T )( ... ), where Type is not the base
* class (Class) or AbstractClass.
*/
'Explicit class definitions must be terminated by an extend call':
function()
{
var _self = this,
Ta = this.Sut( { foo: function() {} } ),
Tb = this.Sut( { bar: function() {} } );
// does not complete with call to `extend'
this.assertThrows( function()
{
_self.Class.use( Ta )();
}, TypeError );
// nested uses; does not complete
this.assertThrows( function()
{
_self.Class.use( Ta ).use( Tb )();
}, TypeError );
// similar to above, with abstract; note that we're checking for
// TypeError here
this.assertThrows( function()
{
_self.AbstractClass.use( Ta )();
}, TypeError );
// does complete; OK
this.assertDoesNotThrow( function()
{
_self.Class.use( Ta ).extend( {} )();
_self.Class.use( Ta ).use( Tb ).extend( {} )();
} );
},
/**
* Ensure that the staging object created by the `implement' call
* exposes a `use' method (and properly applies it).
*/
'Can mix traits into class after implementing interface': function()
{
var _self = this,
called = false,
T = this.Sut( { foo: function() { called = true; } } ),
I = this.Interface( { bar: [] } ),
A = null;
// by declaring this abstract, we ensure that the interface was
// actually implemented (otherwise, all methods would be concrete,
// resulting in an error)
this.assertDoesNotThrow( function()
{
A = _self.AbstractClass.implement( I ).use( T ).extend( {} );
_self.assertOk( A.isAbstract() );
} );
// ensure that we actually fail if there's no interface implemented
// (and thus no abstract members); if we fail and the previous test
// succeeds, that implies that somehow the mixin is causing the
// class to become abstract, and that is an issue (and the reason
// for this seemingly redundant test)
this.assertThrows( function()
{
_self.Class.implement( I ).use( T ).extend( {} );
} );
A.extend( { bar: function() {} } )().foo();
this.assertOk( called );
},
/**
* When a trait is mixed into a class, it acts as though it is part of
* that class. Therefore, it should stand to reason that, when a mixed
* in method returns `this', it should actually return the instance of
* the class that it is mixed into (in the case of this test, its
* private member object, since that's our context when invoking the
* trait method).
*/
'Trait method that returns self will return containing class':
function()
{
var _self = this,
T = this.Sut( { foo: function() { return this; } } );
this.Class.use( T ).extend(
{
go: function()
{
_self.assertStrictEqual( this, this.foo() );
},
} )().go();
},
/**
* Support for static members will be added in future versions; this is
* not something that the author wanted to rush for the first trait
* release, as static members have their own odd quirks.
*/
'Trait static members are prohibited': function()
{
var Sut = this.Sut;
// property
this.assertThrows( function()
{
Sut( { 'static private foo': 'prop' } );
} );
// method
this.assertThrows( function()
{
Sut( { 'static foo': function() {} } );
} );
},
2014-03-11 06:58:39 -04:00
/**
* For the same reasons as static members (described immediately above),
* getters/setters are unsupported until future versions.
*
* Note that we use defineProperty instead of the short-hand object
* literal notation to avoid syntax errors in pre-ES5 environments.
*/
'Trait getters and setters are prohibited': function()
{
// perform these tests only when getters/setters are supported by
// our environment
if ( !( this.hasGetSet ) )
{
return;
}
var Sut = this.Sut;
this.assertThrows( function()
{
var dfn = {};
Object.defineProperty( dfn, 'foo',
{
get: function() {},
set: function() {},
enumerable: true,
} );
Sut( dfn );
} );
},
/**
* The stating object rendered by `#use` calls implement the same
* methods as classes, and are even treated as classes when invoked
* using the immediate syntax (see ImmediateTest). When defining
* abstract classes, staging objects may be extended as if they were
* classes (see AbstractTest).
*
* It makes sense for staging objects to be able to be treated as if
* they were classes, which demands reflection API consistency.
*/
'Staging object for eventual mixin is considered to be class': function()
{
var T = this.Sut( {} );
this.assertOk(
this.Class.isClass( this.Class( {} ).use( T ) )
);
},
/**
* Attempts to mix in non-traits should immediately trigger an error
* during the declaration. It is important not to defer this until the
* time of actual mix in---which is lazy---since the stack will not
* provide useful information on how to correct it.
*/
'Throws error when object to mix in is not a trait': function()
{
var _self = this;
// one of one
this.assertThrows( function()
{
// this should error immediately; it should not wait until
// the actual mix in (which is lazy)
_self.Class( {} ).use( {} );
}, TypeError );
// one of many
this.assertThrows( function()
{
_self.Class( {} ).use( _self.Trait( {} ), {} );
}, TypeError );
},
} );