1  /**
     2   * Copyright the original author or authors.
     3   * 
     4   * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   * 
     8   *      http://www.mozilla.org/MPL/MPL-1.1.html
     9   * 
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  import org.as2lib.core.BasicClass;
    18  import org.as2lib.env.except.IllegalArgumentException;
    19  import org.as2lib.env.except.IllegalStateException;
    20  import org.as2lib.test.mock.MethodCallRangeError;
    21  import org.as2lib.test.mock.MethodBehavior;
    22  import org.as2lib.test.mock.MethodResponse;
    23  import org.as2lib.test.mock.MethodCallRange;
    24  import org.as2lib.test.mock.MethodCall;
    25  import org.as2lib.test.mock.ArgumentsMatcher;
    26  
    27  /**
    28   * {@code DefaultMethodBehavior} stores the expected and actual behaviors of one
    29   * method and verifies the expectation against the actual method calls.
    30   *
    31   * @author Simon Wacker
    32   */
    33  class org.as2lib.test.mock.support.DefaultMethodBehavior extends BasicClass implements MethodBehavior {
    34  	
    35  	/** The expected method call. */
    36  	private var expectedMethodCall:MethodCall;
    37  	
    38  	/** The actual method calls. */
    39  	private var actualMethodCalls:Array;
    40  	
    41  	/** The method call ranges. */
    42  	private var methodCallRanges:Array;
    43  	
    44  	/** The method responses. */
    45  	private var methodResponses:Array;
    46  	
    47  	/**
    48  	 * Constructs a new {@code DefaultMethodBehavior} instance with the passed-in
    49  	 * {@code methodCall}.
    50  	 *
    51  	 * <p>A {@code expectedMethodCall} of value {@code null} means that this behavior
    52  	 * expects no actual method calls.
    53  	 * 
    54  	 * @param expectedMethodCall the expected method call this behavior registers
    55  	 * expectations, actual calls and responses for
    56  	 */
    57  	public function DefaultMethodBehavior(expectedMethodCall:MethodCall) {
    58  		this.expectedMethodCall = expectedMethodCall;
    59  		actualMethodCalls = new Array();
    60  		methodCallRanges = new Array();
    61  		methodResponses = new Array();
    62  	}
    63  	
    64  	/**
    65  	 * Returns the expected method call.
    66  	 *
    67  	 * @return the expected method call
    68  	 */
    69  	public function getExpectedMethodCall(Void):MethodCall {
    70  		return expectedMethodCall;
    71  	}
    72  	
    73  	/**
    74  	 * Adds an actual method call.
    75  	 *
    76  	 * <p>The method call is added if it is not {@code null} and if it matches the
    77  	 * expected method call.
    78  	 *
    79  	 * @param actualMethodCall the new actual method call to add
    80  	 * @throws IllegalArgumentException if the passed-in {@code methodCall} is
    81  	 * {@code null}
    82  	 * @throws AssertionFailedError if no method call was expected or if the
    83  	 * {@code actualMethodCall} does not match the expected method call or if the
    84  	 * total maximum call count has been exceeded
    85  	 */
    86  	public function addActualMethodCall(actualMethodCall:MethodCall):Void {
    87  		if (!actualMethodCall) throw new IllegalArgumentException("Actual method call is not allowed to be null or undefined.", this, arguments);
    88  		if (!expectedMethodCall.matches(actualMethodCall) && expectedMethodCall) {
    89  			var error:MethodCallRangeError = new MethodCallRangeError("Unexpected method call", this, arguments);
    90  			error.addMethodCall(actualMethodCall, new MethodCallRange(0), new MethodCallRange(1));
    91  			error.addMethodCall(expectedMethodCall, new MethodCallRange(getTotalMinimumMethodCallCount(), getTotalMaximumMethodCallCount()), new MethodCallRange(actualMethodCalls.length));
    92  			throw error;
    93  		}
    94  		actualMethodCalls.push(actualMethodCall);
    95  		if (!expectedMethodCall) {
    96  			var error:MethodCallRangeError = new MethodCallRangeError("Unexpected method call", this, arguments);
    97  			error.addMethodCall(actualMethodCall, new MethodCallRange(0), new MethodCallRange(actualMethodCalls.length));
    98  			throw error;
    99  		}
   100  		if (actualMethodCalls.length > getTotalMaximumMethodCallCount()) {
   101  			var error:MethodCallRangeError = new MethodCallRangeError("Unexpected method call", this, arguments);
   102  			error.addMethodCall(actualMethodCall, new MethodCallRange(getTotalMinimumMethodCallCount(), getTotalMaximumMethodCallCount()), new MethodCallRange(actualMethodCalls.length));
   103  			throw error;
   104  		}
   105  	}
   106  	
   107  	/**
   108  	 * Returns the total minimum call count.
   109  	 *
   110  	 * @return the total minimum call count
   111  	 */
   112  	private function getTotalMinimumMethodCallCount(Void):Number {
   113  		if (!expectedMethodCall) return 0;
   114  		if (methodCallRanges.length < 1) return 1;
   115  		var result:Number = 0;
   116  		for (var i:Number = 0; i < methodCallRanges.length; i++) {
   117  			result += MethodCallRange(methodCallRanges[i]).getMinimum();
   118  		}
   119  		return result;
   120  	}
   121  	
   122  	/**
   123  	 * Returns the total maximum call count.
   124  	 *
   125  	 * @return the total maximum call count
   126  	 */
   127  	private function getTotalMaximumMethodCallCount(Void):Number {
   128  		if (!expectedMethodCall) return 0;
   129  		if (methodCallRanges.length < 1) return 1;
   130  		var result:Number = 0;
   131  		for (var i:Number = 0; i < methodCallRanges.length; i++) {
   132  			result += MethodCallRange(methodCallRanges[i]).getMaximum();
   133  		}
   134  		return result;
   135  	}
   136  
   137  	/**
   138  	 * Adds the new {@code methodResponse} together with the {@code methodCallRange}
   139  	 * that indicates when and how often the response shall take place.
   140  	 *
   141  	 * <p>If you set no response, the behavior expects exactly one method call.
   142  	 *
   143  	 * @param methodResponse the response to do a given number of times
   144  	 * @param methodCallRange the range that indicates how often the response can take
   145  	 * place
   146  	 *
   147  	 * @throws IllegalStateException if the expected method call is {@code null}
   148  	 */
   149  	public function addMethodResponse(methodResponse:MethodResponse, methodCallRange:MethodCallRange):Void {
   150  		if (!expectedMethodCall) throw new IllegalStateException("It is not possible to set a response for an not-expected method call.", this, arguments);
   151  		methodResponses.push(methodResponse);
   152  		methodCallRanges.push(methodCallRange);
   153  	}
   154  	
   155  	/**
   156  	 * Sets the passed-in {@code argumentsMatcher} for the expected method call.
   157  	 * 
   158  	 * @param argumentsMatcher the arguments matcher for the expected method call
   159  	 */
   160  	public function setArgumentsMatcher(argumentsMatcher:ArgumentsMatcher):Void {
   161  		expectedMethodCall.setArgumentsMatcher(argumentsMatcher);
   162  	}
   163  	
   164  	/**
   165  	 * Checks whether this behavior expects another method call.
   166  	 *
   167  	 * @return {@code true} if a further method call is expected else {@code false}
   168  	 */
   169  	public function expectsAnotherMethodCall(Void):Boolean {
   170  		if (!expectedMethodCall) return false;
   171  		if (methodCallRanges.length < 1) {
   172  			if (actualMethodCalls.length < 1) return true;
   173  			else return false;
   174  		}
   175  		return (getCurrentMethodCallRangeIndex() > -1);
   176  	}
   177  	
   178  	/**
   179  	 * Returns the current position in the method call range array.
   180  	 *
   181  	 * @return the current position in the method call range array
   182  	 */
   183  	private function getCurrentMethodCallRangeIndex(Void):Number {
   184  		var maximum:Number = 0;
   185  		for (var i:Number = 0; i < methodCallRanges.length; i++) {
   186  			var methodCallRange:MethodCallRange = methodCallRanges[i];
   187  			if (methodCallRange) {
   188  				maximum += methodCallRange.getMaximum();
   189  			} else {
   190  				maximum += Number.POSITIVE_INFINITY;
   191  			}
   192  			if (actualMethodCalls.length < maximum) {
   193  				return i;
   194  			}
   195  		}
   196  		return -1;
   197  	}
   198  	
   199  	/**
   200  	 * Responses depending on the current number of actual method calls.
   201  	 *
   202  	 * @return the response's return value
   203  	 * @throw the response's throwable
   204  	 */
   205  	public function response(Void) {
   206  		return MethodResponse(methodResponses[getCurrentMethodResponseIndex()]).response();
   207  	}
   208  	
   209  	/**
   210  	 * Returns the current position in the method response array.
   211  	 *
   212  	 * @return the current position in the method response array
   213  	 */
   214  	private function getCurrentMethodResponseIndex(Void):Number {
   215  		var maximum:Number = 0;
   216  		for (var i:Number = 0; i < methodCallRanges.length; i++) {
   217  			maximum += MethodCallRange(methodCallRanges[i]).getMaximum();
   218  			if (actualMethodCalls.length <= maximum) {
   219  				return i;
   220  			}
   221  		}
   222  		return -1;
   223  	}
   224  	
   225  	/**
   226  	 * Verifies that the expactations have been met.
   227  	 *
   228  	 * @throws AssertionFailedError if the verification fails
   229  	 */
   230  	public function verify(Void):Void {
   231  		if (actualMethodCalls.length < getTotalMinimumMethodCallCount()) {
   232  			var error:MethodCallRangeError = new MethodCallRangeError("Expectation failure on verify", this, arguments);
   233  			error.addMethodCall(expectedMethodCall, new MethodCallRange(getTotalMinimumMethodCallCount(), getTotalMaximumMethodCallCount()), new MethodCallRange(actualMethodCalls.length));
   234  			throw error;
   235  		}
   236  	}
   237  	
   238  }