// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed 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 com.google.collide.client.testing; import com.google.collide.client.communication.FrontendApi.ApiCallback; import com.google.collide.client.testing.MockFrontendApi.MockApi; import com.google.collide.dto.ServerError; import com.google.collide.dto.ServerError.FailureReason; import com.google.collide.dtogen.shared.ClientToServerDto; import com.google.collide.dtogen.shared.RoutableDto; import com.google.collide.dtogen.shared.ServerToClientDto; import com.google.common.annotations.VisibleForTesting; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; /** * Canned expectations, pairing an expected message with either a simulated * response ({@link FrontendExpectation.Response}), simulated server failure * ({@link FrontendExpectation.Fail}), or a simulated client-side exception * ({@link FrontendExpectation.Throw}). * * These are used in the {@link MockApi} class. * * * @param <REQ> request type for the expectation * @param <RESP> correct response type for the expectation */ abstract class FrontendExpectation< REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends Expectation<REQ, RESP> { /** * Expectation for a server-side error instead of a correct message. * * @param <REQ> request type * @param <RESP> correct response type (of the contract, not the thrown * exception type) */ public static class Fail<REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends FrontendExpectation<REQ, RESP> { @SuppressWarnings("unused") private ServerError error; public Fail(REQ req, ServerError error) { super(req); this.error = error; } @Override public void doCallback(final ApiCallback<RESP> callback) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { // did we really mean to lose our thrown info? callback.onFail(FailureReason.COMMUNICATION_ERROR); } }); } } /** * Expectation for a server-side error due to a communication error. * * @param <REQ> request type */ public static class CommunicationFailure< REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends FrontendExpectation< REQ, RESP> { public CommunicationFailure(REQ request) { super(request); } @Override public void doCallback(final ApiCallback<RESP> callback) { callback.onFail(FailureReason.COMMUNICATION_ERROR); } } /** * Expectation for a correct message response. * * @param <REQ> request type * @param <RESP> response type */ public static class Response<REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends FrontendExpectation<REQ, RESP> { private RESP response; public Response(REQ req, RESP response) { super(req); this.response = response; } @Override public void doCallback(final ApiCallback<RESP> callback) { callback.onMessageReceived(response); } } /** * Expectation for a correct message response, delivered asynchronously. * * @param <REQ> request type * @param <RESP> response type */ public static class AsyncResponse<REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends FrontendExpectation<REQ, RESP> { private RESP response; public AsyncResponse(REQ req, RESP response) { super(req); this.response = response; } @Override public void doCallback(final ApiCallback<RESP> callback) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { callback.onMessageReceived(response); } }); } } /** * Expectation for a thrown exception instead of a correct message. * * @param <REQ> request type * @param <RESP> correct response type (of the contract, not the thrown * exception type) */ public static class Throw<REQ extends ClientToServerDto, RESP extends ServerToClientDto> extends FrontendExpectation<REQ, RESP> { @SuppressWarnings("unused") private Throwable response; public Throw(REQ req, Throwable response) { super(req); this.response = response; } @Override public void doCallback(final ApiCallback<RESP> callback) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { // did we really mean to lose our thrown info? callback.onFail(FailureReason.COMMUNICATION_ERROR); } }); } } public FrontendExpectation(REQ request) { super(request); } /** * Throws a runtime exception if the given request isn't the expected value. * * @param request actual request to test against expection */ public void checkExpectation(REQ request) { String check = checkMatch(this.request, request); if (check.length() > 0) { throw new ExpectationViolation(check); } } /** * Does whatever this expectation type does with the callback object. * * @param callback */ public abstract void doCallback(ApiCallback<RESP> callback); /** * Tests two message objects for "equality," not identity or class-identity, * by checking that all the object fields match except Chrome's __gwt_ObjectId * * To allow some looseness in the matching, the check is not symmetric. Any * properties set in the first, pattern object must match those in the second, * target argument, but the reverse is not true. * * @param pattern the "pattern" objects, the properties of which have to * match those in the {@code target} object. * @param target the "actual" object in the comparison. This may have extra * properties not checked by the {@code pattern}, but where they do overlap, * the {@code target} properties must match the {@code pattern}. * @return an empty string if the objects do match, or a text identifying the * mismatch(es) if they do not. */ @VisibleForTesting static native String checkMatch(RoutableDto pattern, RoutableDto target) /*-{ var result = new Array(); // some special handling for arrays, for which we don't want "extra is okay" if (pattern instanceof Array) { if (! target instanceof Array) { result.push("expected array, got non-array object"); } else if (pattern.length != target.length) { result.push("expected array length " + pattern.length + ", got array of length " + target.length); } } for (prop in pattern) { if (pattern.hasOwnProperty(prop)) { if (typeof(pattern[prop]) == 'object') { if (typeof(target[prop]) != 'object') { result.push(" field " + prop + " is not an object, but " + typeof(target[prop])); } else { // using a simple != fails, it gives identity not equivalence. // so we recurse checking property equivalence: var fail = @com.google.collide.client.testing.FrontendExpectation::checkMatch(Lcom/google/collide/dtogen/shared/RoutableDto;Lcom/google/collide/dtogen/shared/RoutableDto;) (pattern[prop], target[prop]); if (fail != "") { result.push("in field " + prop + ": " + fail); } } } else if (typeof(target[prop]) == 'undefined' || pattern[prop] != target[prop]) { result.push(" field " + prop + " does not match: " + pattern[prop] + " != " + target[prop]); } } } return result.toString(); }-*/; }