FlexUnit4/src/org/flexunit/internals/runners/statements/ExpectAsync.as (585 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.flexunit.internals.runners.statements { import flash.events.Event; import flash.net.Responder; import flash.utils.Dictionary; import mx.events.PropertyChangeEvent; import mx.rpc.IResponder; import org.flexunit.Assert; import org.flexunit.AssertionError; import org.flexunit.async.AsyncHandler; import org.flexunit.async.AsyncLocator; import org.flexunit.async.AsyncNativeTestResponder; import org.flexunit.async.AsyncTestResponder; import org.flexunit.async.IAsyncTestResponder; import org.flexunit.async.ITestResponder; import org.flexunit.constants.AnnotationArgumentConstants; import org.flexunit.constants.AnnotationConstants; import org.flexunit.events.AsyncEvent; import org.flexunit.events.AsyncResponseEvent; import org.flexunit.internals.flexunit_internal; import org.flexunit.runners.model.FrameworkMethod; import org.flexunit.runners.model.TestClass; import org.flexunit.token.AsyncTestToken; import org.flexunit.token.ChildResult; import org.flexunit.utils.ClassNameUtil; import org.fluint.sequence.SequenceBindingWaiter; import org.fluint.sequence.SequenceRunner; use namespace flexunit_internal; /** * The <code>ExpectAsync</code> is a decorator that is responsible for determing * whether a specific test method is expecting an asynchronous response. As this * infrastructure carries overhead, we only create it when the user specifics a * given test is asynchronous. The class implements <code>IAsyncHandlingStatement</code> * and works with the <code>Async</code> class.<br/> * * In order to expect an asynchronous response, a test method must include metadata indicating * it is expecting asynchronous functionality.<br/> * * <pre><code> * [Test(async)] * public function asyncTest():void { * //Test with asynchronous functionality * } * </code></pre> * * @see org.flexunit.async.Async */ public class ExpectAsync extends AsyncStatementBase implements IAsyncStatement, IAsyncHandlingStatement { /** * @private */ private var objectUnderTest:Object; /** * @private */ private var statement:IAsyncStatement; /** * @private */ private var returnMessageSent:Boolean = false; /** * @private */ private var testComplete:Boolean; /** * @private */ private var pendingAsyncCalls:Array; /** * @private */ private var asyncFailureConditions:Dictionary; /** * @private */ private var methodBodyExecuting:Boolean = false; /** * Returns a Boolean value indicating whether the test method is current executing. */ public function get bodyExecuting():Boolean { return methodBodyExecuting; } /** * Returns a Boolean value indicating whether there are still any pending asynchronous calls. */ public function get hasPendingAsync():Boolean { return ( pendingAsyncCalls.length > 0 ); } /** * Attempts to call the provided <code>method</code> with the provided <code>rest</code> parameters. * * @param method The Function to call with the provided <code>rest</code> parameters. * @param rest The parameters supplied to the <code>method</code>. */ protected function protect( method:Function, ... rest ):void { try { if ( rest && rest.length>0 ) { method.apply( this, rest ); } else { method(); } if ( hasPendingAsync ) { startAsyncTimers(); } } catch (error:Error) { sendComplete( error ); } } /** * Removes asynchronous event listeners from the provided <code>asyncHandler</code>. * * @param asyncHandler The <code>AsyncHandler</code> from which to remove the event listeners. */ private function removeAsyncEventListeners( asyncHandler:AsyncHandler ):void { asyncHandler.removeEventListener( AsyncHandler.EVENT_FIRED, handleAsyncEventFired, false ); asyncHandler.removeEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false ); } /** * Removes asynchronous error event listeners from the provided <code>asyncHandler</code>. * * @param asyncHandler The <code>AsyncHandler</code> from which to remove the error event listeners. */ private function removeAsyncErrorEventListeners( asyncHandler:AsyncHandler ):void { asyncHandler.removeEventListener( AsyncHandler.EVENT_FIRED, handleAsyncErrorFired, false ); asyncHandler.removeEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false ); } /** * * * * @internal TODO:: This needs to be cleaned up and revised... just a prototype * @param eventHandler * @param timeout * @param passThroughData * @param timeoutHandler * * @throws Error Test Completed, but additional async event added * * @return * */ public function asyncErrorConditionHandler( eventHandler:Function ):Function { if ( testComplete ) { sendComplete( new Error("Test Completed, but additional async event added") ); } var asyncHandler:AsyncHandler = new AsyncHandler( eventHandler ) asyncHandler.addEventListener( AsyncHandler.EVENT_FIRED, handleAsyncErrorFired, false, 0, true ); //asyncHandler.addEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false, 0, true ); asyncFailureConditions[ asyncHandler ] = true; return asyncHandler.handleEvent; } /** * Creates an <code>AsyncHandler</code> that pend and either call the <code>eventHandler</code> or the * <code>timeoutHandler</code>, passing the <code>passThroughData</code>, depending on whether the * <code>timeout</code> period has been reached. * * @param eventHandler The Function that will be executed if the handler is called before * the <code>timeout</code> has expired. * @param timeout The length of time, in milliseconds, before the <code>timeoutHandler</code> will be executed. * @param passThroughData An Object that can be given information about the current test; this information will * be available for both the <code>eventHandler</code> and the <code>timeoutHandler</code>. * @param timeoutHandler The Function that will be executed if the <code>timeout</code> time is reached prior to * the expected event being dispatched. * * @return an event handler Function that will determine whether the <code>timeout</code> has been reached. */ public function asyncHandler( eventHandler:Function, timeout:int, passThroughData:Object = null, timeoutHandler:Function = null ):Function { if ( testComplete ) { sendComplete( new Error("Test Completed, but additional async event added") ); } var asyncHandler:AsyncHandler = new AsyncHandler( eventHandler, timeout, passThroughData, timeoutHandler ) asyncHandler.addEventListener( AsyncHandler.EVENT_FIRED, handleAsyncEventFired, false, 0, true ); asyncHandler.addEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false, 0, true ); pendingAsyncCalls.push( asyncHandler ); return asyncHandler.handleEvent; } // We have a toggle in the compiler arguments so that we can choose whether or not the flex classes should // be compiled into the FlexUnit swc. For actionscript only projects we do not want to compile the // flex classes since it will cause errors. /** * Creates an <code>IAsyncTestResponder</code> that pend and either call the <code>eventHandler</code> or the * <code>timeoutHandler</code>, passing the <code>passThroughData</code>, depending on whether the * <code>timeout</code> period has been reached. * * @param responder The responder that will be executed if the <code>IResponder</code> is called before * the <code>timeout</code> has expired. * @param timeout The length of time, in milliseconds, before the <code>timeoutHandler</code> will be executed. * @param passThroughData An Object that can be given information about the current test; this information will * be available for both the <code>eventHandler</code> and the <code>timeoutHandler</code>. * @param timeoutHandler The Function that will be executed if the <code>timeout</code> time is reached prior to * the expected event being dispatched. * * @return an <code>IResponder</code> that will determine whether the <code>timeout</code> has been reached. */ CONFIG::useFlexClasses public function asyncResponder( responder:*, timeout:int, passThroughData:Object = null, timeoutHandler:Function = null ):IResponder { var asyncResponder:IAsyncTestResponder; if ( !( ( responder is IResponder ) || ( responder is ITestResponder ) ) ) { throw new Error( "Object provided to responder parameter of asyncResponder is not a IResponder or ITestResponder" ); } /**If the user passes use an IAsyncTestResponder of their own, then we do not need to wrap it in our own AsyncTestResponder class * This allows the use of a different type of responder than our standard, however, it is the responsibility of the IAsyncTestResponder * we are passed to dispatch the requisite AsyncResponseEvent events in response to a result or fault. * * In your own code, you can therefore do something like: * * asyncResponder( IResponder, timeout ); * * OR * * asyncResponder( mySpecialResponder implements IAsyncTestResponder, timeout ); * * */ if ( responder is IAsyncTestResponder ) { asyncResponder = responder; } else { asyncResponder = new AsyncTestResponder( responder ); } var asyncHandler:AsyncHandler = new AsyncHandler( handleAsyncTestResponderEvent, timeout, passThroughData, timeoutHandler ) asyncHandler.addEventListener( AsyncHandler.EVENT_FIRED, handleAsyncEventFired, false, 0, true ); asyncHandler.addEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false, 0, true ); pendingAsyncCalls.push( asyncHandler ); asyncResponder.addEventListener( AsyncResponseEvent.RESPONDER_FIRED, asyncHandler.handleEvent, false, 0, true ); return asyncResponder; } /** * Removes all asynchronous event listeners for each pending asynchronous call. */ private function removeAllAsyncEventListeners():void { for ( var i:int=0; i<pendingAsyncCalls.length; i++ ) { removeAsyncEventListeners( pendingAsyncCalls[ i ] as AsyncHandler ); } pendingAsyncCalls = new Array(); for ( var handler:* in asyncFailureConditions ) { removeAsyncErrorEventListeners( handler as AsyncHandler ); } asyncFailureConditions = new Dictionary( true ); } /** * Called when the asynchronous timeout has been reached for a given test. This method attempts to * call a timeout handler that has been associated with the asychronous test. If no handler has * been specified, an error will be generated and the <code>ExpectAsync</code> statement finishes. * * @param event The event associated with the asynchronous timeout. */ private function handleAsyncTimeOut( event:Event ):void { var asyncHandler:AsyncHandler = event.target as AsyncHandler; var failure:Boolean = false; removeAsyncEventListeners( asyncHandler ); //Run the timeout handler if one exists; otherwise, send an error if ( asyncHandler.timeoutHandler != null ) { protect( asyncHandler.timeoutHandler, asyncHandler.passThroughData ); } else { failure = true; sendComplete( new AssertionError( "Timeout Occurred before expected event" ) ); //protect( Assert.fail, "Timeout Occurred before expected event" ); } //Remove all future pending items removeAllAsyncEventListeners(); /** var methodResult:TestMethodResult = testMonitor.getTestMethodResult( registeredMethod ); if ( methodResult && ( !methodResult.traceInformation ) ) { methodResult.executed = true; methodResult.testDuration = getTimer()-tickCountOnStart; methodResult.traceInformation = "Test completed via Async TimeOut in " + methodResult.testDuration + "ms"; } **/ //Our timeout has failed, declare this specific test complete and move along sendComplete(); } /** * Handles the AsyncResponseEvent that is thrown by the asyncResponder. * It sends data to the original responder based on if it is a result or fault status. * * If the original responder is of type ITestResponder, then the passThroughData is passed to it. * * @param event * @param passThroughData * */ protected function handleAsyncTestResponderEvent( event:AsyncResponseEvent, passThroughData:Object=null ):void { var originalResponder:* = event.originalResponder; var isTestResponder:Boolean = false; if ( originalResponder is ITestResponder ) { isTestResponder = true; } if ( event.status == 'result' ) { if ( isTestResponder ) { originalResponder.result( event.data, passThroughData ); } else { originalResponder.result( event.data ); } } else { if ( isTestResponder ) { originalResponder.fault( event.data, passThroughData ); } else { originalResponder.fault( event.data ); } } } private function handleAsyncErrorFired( event:AsyncEvent ):void { var message:String = "Failing due to Async Event "; if ( event && event.originalEvent ) { message += String( event.originalEvent ); } sendComplete( new AssertionError( message ) ); } /** * Called when the asynchronous event has been fired prior to the timeout being reached. This method attempts to * call event handler that has been associated with the asychronous test. If no handler has * been specified, an error will be generated and the <code>ExpectAsync</code> statement finishes. * * @param event The <code>AsyncEvent</code> event that has been dispatched. */ private function handleAsyncEventFired( event:AsyncEvent ):void { //Receiving this event is a good things... IF it is the first one we are waiting for //If it is not the first one on the stack though, we still need to fail. var asyncHandler:AsyncHandler = event.target as AsyncHandler; var firstPendingAsync:AsyncHandler; var failure:Boolean = false; removeAsyncEventListeners( asyncHandler ); //Determine if any async calls remain if ( hasPendingAsync ) { //Get the first async call firstPendingAsync = pendingAsyncCalls.shift() as AsyncHandler; //Determine if this was the expected async handler if ( firstPendingAsync === asyncHandler ) { if ( asyncHandler.eventHandler != null ) { //this actually needs to be the event object from the previous event protect( asyncHandler.eventHandler, event.originalEvent, firstPendingAsync.passThroughData ); } } else { //The first one on the stack is not the one we received. //We received this one out of order, which is a failure condition failure = true; sendComplete( new AssertionError( "Asynchronous Event Received out of Order" ) ); //protect( Assert.fail, "Asynchronous Event Received out of Order" ); } } else { //We received an event, but we were not waiting for one, failure //protect( Assert.fail, "Unexpected Asynchronous Event Occurred" ); failure = true; sendComplete( new AssertionError( "Unexpected Asynchronous Event Occurred" ) ); } if ( !hasPendingAsync && !methodBodyExecuting && !failure ) { //We have no more pending async, *AND* the method body of the function that originated this message //has also finished, then let the test runner know /** var methodResult:TestMethodResult = testMonitor.getTestMethodResult( registeredMethod ); if ( methodResult && ( !methodResult.traceInformation ) ) { methodResult.executed = true; methodResult.testDuration = getTimer()-tickCountOnStart; methodResult.traceInformation = "Test completed via Async Event in " + methodResult.testDuration + "ms"; } **/ sendComplete(); } } /** * Handles the next steps in a <code>SequenceRunner</code>. * * @param event The event boradcast by the last step in the sequence. * @param sequenceRunner The runner responsible for running the steps in the sequence. */ public function handleNextSequence( event:Event, sequenceRunner:SequenceRunner ):void { if ( event && event.target ) { //Remove the listener for this particular item event.currentTarget.removeEventListener(event.type, handleNextSequence ); } sequenceRunner.continueSequence( event ); startAsyncTimers(); } /** * Creates an <code>IAsyncTestResponder</code> that pend and either call the <code>eventHandler</code> or the * <code>timeoutHandler</code>, passing the <code>passThroughData</code>, depending on whether the * <code>timeout</code> period has been reached. * * @param resultHandler The result <code>Function</code> that will be executed if the <code>Responder</code> is called on its result before * the <code>timeout</code> has expired. * @param faultHandler The fault <code>Function</code> that will be executed if the <code>Responder</code> is called on its fault before * the <code>timeout</code> has expired. * @param timeout The length of time, in milliseconds, before the <code>timeoutHandler</code> will be executed. * @param passThroughData An Object that can be given information about the current test; this information will * be available for both the <code>eventHandler</code> and the <code>timeoutHandler</code>. * @param timeoutHandler The Function that will be executed if the <code>timeout</code> time is reached prior to * the expected event being dispatched. * * @return an <code>IResponder</code> that will determine whether the <code>timeout</code> has been reached. */ public function asyncNativeResponder( resultHandler : Function, faultHandler : Function, timeout:int, passThroughData:Object = null, timeoutHandler:Function = null ):Responder { var asyncResponder:AsyncNativeTestResponder; asyncResponder = new AsyncNativeTestResponder( resultHandler, faultHandler ); var asyncHandler:AsyncHandler = new AsyncHandler( handleAsyncNativeTestResponderEvent, timeout, passThroughData, timeoutHandler ) asyncHandler.addEventListener( AsyncHandler.EVENT_FIRED, handleAsyncEventFired, false, 0, true ); asyncHandler.addEventListener( AsyncHandler.TIMER_EXPIRED, handleAsyncTimeOut, false, 0, true ); pendingAsyncCalls.push( asyncHandler ); asyncResponder.addEventListener( AsyncResponseEvent.RESPONDER_FIRED, asyncHandler.handleEvent, false, 0, true ); return asyncResponder; } /** * Handles the AsyncResponseEvent that is thrown by the asyncResponder. * It sends data to the original responder based on if it is a result or fault status. * * @param event * @param passThroughData * */ protected function handleAsyncNativeTestResponderEvent( event:AsyncResponseEvent, passThroughData:Object=null ):void { var methodHandler:Function = event.methodHandler; methodHandler.call(this, event.data); } /** * * @param event * @param sequenceRunner * */ // We have a toggle in the compiler arguments so that we can choose whether or not the flex classes should // be compiled into the FlexUnit swc. For actionscript only projects we do not want to compile the // flex classes since it will cause errors. CONFIG::useFlexClasses public function handleBindableNextSequence( event:Event, sequenceRunner:SequenceRunner ):void { if ( sequenceRunner.getPendingStep() is SequenceBindingWaiter ) { var sequenceBinding:SequenceBindingWaiter = sequenceRunner.getPendingStep() as SequenceBindingWaiter; if (event is PropertyChangeEvent) { var propName:Object = PropertyChangeEvent(event).property if (propName != sequenceBinding.propertyName) { Assert.fail( "Incorrect Property Change Event Received" ); } } if ( event && event.target ) { //Remove the listener for this particular item sequenceBinding.changeWatcher.unwatch(); //event.currentTarget.removeEventListener(event.type, handleBindableNextSequence ); sequenceRunner.continueSequence( event ); startAsyncTimers(); } } else { Assert.fail( "Event Received out of Order" ); } } /** * Starts the timers for each pending asynchronous call. */ private function startAsyncTimers():void { for ( var i:int=0; i<pendingAsyncCalls.length; i++ ) { ( pendingAsyncCalls[ i ] as AsyncHandler ).startTimer(); } } /** * If the test has not yet been marked as complete, mark the <code>ExpectAsync</code> statement as having finished * and notify the parent token. * * @param error The potential error to send to the parent token. */ override protected function sendComplete( error:Error = null ):void { //If the test has not completed, do not notify the parentToken that this statement has finished executing if ( !testComplete ) { methodBodyExecuting = false; testComplete = true; AsyncLocator.cleanUpCallableForTest( getObjectForRegistration( objectUnderTest ) ); removeAllAsyncEventListeners(); parentToken.sendResult( error ); } } /** * Retrieves the object used for registering this <code>ExpectAsync</code> which * implements <code>IAsyncHandlingStatement</code> * * @param obj The object used to obtain the registration object. */ private function getObjectForRegistration( obj:Object ):Object { var registrationObj:Object; if ( obj is TestClass ) { registrationObj = ( obj as TestClass ).asClass; } else { registrationObj = obj; } return registrationObj; } /** * Registers the <code>ExpectAsync</code> statment for the current object being tested and evaluates the object that * implements the <code>IAsyncStatement</code> that was provided to the <code>ExpectAsync</code> class. * * @param parentToken The token to be notified when the potential asynchronous call have finished. */ public function evaluate( parentToken:AsyncTestToken ):void { this.parentToken = parentToken; //Register this statement with the current object that is being tested AsyncLocator.registerStatementForTest( this, getObjectForRegistration( objectUnderTest ) ); methodBodyExecuting = true; statement.evaluate( myToken ); methodBodyExecuting = false; } /** * A handler method that is called in order to wait once an asynchronous event has been dispatched. * * @param event The event that was received. * @param passThroughData An Object that contains information to pass to the handler. */ public function pendUntilComplete( event:Event, passThroughData:Object=null ):void { } /** * A handler method that is called in order to fail for a given asynchronous event once an it * has been dispatched. * * @param event The event that was received. * @param passThroughData An Object that contains information to pass to the handler. */ public function failOnComplete( event:Event, passThroughData:Object ):void { var message:String = "Unexpected event received "; if ( event ) { message += String( event ); } sendComplete( new AssertionError( message ) ); } /** * Determines if there are any more pending asynchronous calls; if there are, keep running the calls if there are * no errors. If all calls have finished or an error was encountered in the <code>result</code>, report the error * to the parent token and stop tracking the asynchronous events. * * @param result The <code>ChildResult</code> to check to see if there is an error. */ public function handleNextExecuteComplete( result:ChildResult ):void { if ( pendingAsyncCalls.length == 0 ) { //we are all done, no more pending asyncs, we can go on and live a good life for the few moments before we are gced sendComplete( result.error ); //parentToken.sendResult( result.error ); } else { if ( result && result.error ) { //If we already have an error, we need to report it now, not coninue sendComplete( result.error ); } else { startAsyncTimers(); } } } /** * Determine if the <code>method</code> is asynchronous for a test method that is of the expected * metadata <code>type</code>. * * @param method The <code>FrameworkMethod</code> that is potentially asynchronous. * @param type The expected metadata type of the test method (ex: "Test", "Before", "After"). * * @return a Boolean value indicating whether the provided <code>method</code> is asynchronous. */ public static function hasAsync( method:FrameworkMethod, type:String=AnnotationConstants.TEST ):Boolean { var async:String = method.getSpecificMetaDataArgValue( type, AnnotationArgumentConstants.ASYNC ); var asyncBool:Boolean = ( async == "true" ); return asyncBool; } /** * Constructor. * * @param objectUnderTest The current object that is being tested. * @param statement The current <code>IAsyncStatement</code> that will be decorated with the * the <code>ExpectAsync</code> class. */ public function ExpectAsync( objectUnderTest:Object, statement:IAsyncStatement ) { this.objectUnderTest = objectUnderTest; this.statement = statement; //Create a new token that will alert this class when the provided statement has completed myToken = new AsyncTestToken( ClassNameUtil.getLoggerFriendlyClassName( this ) ); myToken.addNotificationMethod( handleNextExecuteComplete ); pendingAsyncCalls = new Array(); asyncFailureConditions = new Dictionary( true ); } } }