/** * Tests overriding virtual class methods using mixins * * 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 . * * These tests vary from those in VirtualTest in that, rather than a class * overriding a virtual method defined within a trait, a trait is overriding * a method in the class that it is mixed into. In particular, since * overrides require that the super method actually exist, this means that a * trait must implement or extend a common interface. * * It is this very important (and powerful) system that allows traits to be * used as stackable modifications, similar to how one would use the * decorator pattern (but more tightly coupled). * * See also ClassExtendTest, which is related in that it can too define * virtual methods. */ require( 'common' ).testCase( { caseSetUp: function() { this.Sut = this.require( 'Trait' ); this.Class = this.require( 'class' ); this.AbstractClass = this.require( 'class_abstract' ); this.Interface = this.require( 'interface' ); }, /** * This is a concept borrowed from Scala: consider class C and trait T, * both implementing interface I which declares method M. T should be * able to override C.M so long as it is concrete, but to do so, we need * some way of telling ease.js that we are overriding at time of mixin; * otherwise, override does not make sense, because I.M is clearly * abstract and there is nothing to override. */ 'Mixin can override virtual concrete method defined by interface': function() { var called = false, I = this.Interface( { foo: [] } ); var T = this.Sut.implement( I ).extend( { // the keyword combination `abstract override' indicates that we // should override whatever concrete implementation was defined // before our having been mixed in 'abstract override foo': function() { called = true; }, } ); var _self = this; var C = this.Class.implement( I ).extend( { // this should be overridden by the mixin and should therefore // never be called (for __super tests, see LinearizationTest) 'virtual foo': function() { _self.fail( false, true, "Concrete class method was not overridden by mixin" ); }, } ); // mixing in a trait atop of C should yield the results described // above due to the `abstract override' keyword combination C.use( T )().foo(); this.assertOk( called ); }, /** * Virtual methods for traits are handled via a series of proxy methods * that determine, at runtime (as opposed to when the class is created), * where the call should go. (At least that was the implementation at * the time this test was written.) This test relies on the proper * parameter metadata being set on those proxy methods so that the * necessary length requirements can be validated. * * This was a bug in the initial implemenation: the above tests did not * catch it because the virtual methods had no arguments. The initial * problem was that, since __length was not defined on the generated * method that was recognized as the override, it was always zero, which * always failed if there were any arguments on the virtual method. The * reverse case was also a problem, but it didn't manifest as an * error---rather, it did *not* error when it should have. * * Note the instantiation in these cases: this is because the trait * implementation lazily performs the mixin on first use. */ 'Subtype must meet compatibility requirements of virtual trait method': function() { var _self = this; var C = this.Class.use( this.Sut( { 'virtual foo': function( a, b ) {} } ) ); this.assertThrows( function() { // does not meet param requirements (note the // instantiation---traits defer processing until they are used) C.extend( { 'override foo': function( a ) {} } )(); } ); this.assertDoesNotThrow( function() { // does not meet param requirements (note the // instantiation---traits defer processing until they are used) C.extend( { 'override foo': function( a, b ) {} } )(); } ); }, } );