blob: bf2a6bf234ddca94bf298dc5b4ce506b4dc1c5a0 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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.gwt.junit.client.impl;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.http.client.UrlBuilder;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.junit.client.impl.JUnitHost.ClientInfo;
import com.google.gwt.junit.client.impl.JUnitHost.InitialResponse;
import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamFactory;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import java.util.HashMap;
/**
* The entry point class for GWTTestCases.
*
* This is the main test running logic. Each time a test completes, the results
* are reported back through {@link #junitHost}, and the next method to run is
* returned. This process repeats until the next method to run is null.
*/
public abstract class GWTRunner implements EntryPoint {
private final class InitialResponseListener implements
AsyncCallback<InitialResponse> {
/**
* Delegate to the {@link TestBlockListener}.
*/
public void onFailure(Throwable caught) {
testBlockListener.onFailure(caught);
}
/**
* Update our client info with the server-provided session id then delegate
* to the {@link TestBlockListener}.
*/
public void onSuccess(InitialResponse result) {
clientInfo = new ClientInfo(result.getSessionId(),
clientInfo.getUserAgent());
testBlockListener.onSuccess(result.getTestBlock());
}
}
/**
* The RPC callback object for {@link GWTRunner#junitHost}. When
* {@link #onSuccess} is called, it's time to run the next test case.
*/
private final class TestBlockListener implements AsyncCallback<TestBlock> {
/**
* The number of times we've failed to communicate with the server on the
* current test batch.
*/
private int curRetryCount = 0;
/**
* A call to junitHost failed.
*/
public void onFailure(Throwable caught) {
if (maxRetryCount < 0 || curRetryCount < maxRetryCount) {
// Try the call again
curRetryCount++;
new Timer() {
@Override
public void run() {
syncToServer();
}
}.schedule(1000);
} else {
// Give up and mark the test complete on the client side.
markComplete();
}
}
/**
* A call to junitHost succeeded; run the next test case.
*/
public void onSuccess(TestBlock nextTestBlock) {
curRetryCount = 0;
currentBlock = nextTestBlock;
currentTestIndex = 0;
currentResults.clear();
if (currentBlock != null && currentBlock.getTests().length > 0) {
doRunTest();
} else {
markComplete();
}
}
/**
* Set a global expando so the test infrastructure knows that the test is
* complete.
*/
private native void markComplete() /*-{
$doc.title = "Completed Tests";
$wnd._gwt_test_complete = true;
}-*/;
}
/**
* The singleton instance.
*/
static GWTRunner sInstance;
/**
* A query param specifying my unique session cookie.
*/
private static final String SESSIONID_QUERY_PARAM = "gwt.junit.sessionId";
/**
* A query param specifying the test class to run, for serverless mode.
*/
private static final String TESTCLASS_QUERY_PARAM = "gwt.junit.testclassname";
/**
* A query param specifying the test method to run, for serverless mode.
*/
private static final String TESTFUNC_QUERY_PARAM = "gwt.junit.testfuncname";
/**
* A query param specifying the number of times to retry if the server fails
* to respond.
*/
private static final String RETRYCOUNT_QUERY_PARAM = "gwt.junit.retrycount";
/**
* A query param specifying the block index to start on.
*/
private static final String BLOCKINDEX_QUERY_PARAM = "gwt.junit.blockindex";
public static GWTRunner get() {
return sInstance;
}
/**
* Convert unserializable exceptions (usually from dev mode) into generic
* serializable ones.
*/
private static void ensureSerializable(ExceptionWrapper wrapper,
SerializationStreamWriter writer) {
if (wrapper == null) {
return;
}
ensureSerializable(wrapper.causeWrapper, writer);
try {
writer.writeObject(wrapper.exception);
} catch (SerializationException e) {
wrapper.exception = new Exception(wrapper.exception.toString());
}
}
/**
* This client's info.
*/
private ClientInfo clientInfo;
/**
* The current block of tests to execute.
*/
private TestBlock currentBlock;
/**
* Active test within current block of tests.
*/
private int currentTestIndex = 0;
/**
* Results for all test cases in the current block.
*/
private HashMap<TestInfo, JUnitResult> currentResults = new HashMap<TestInfo, JUnitResult>();
/**
* If set, all remaining tests will fail with the failure message.
*/
private String failureMessage;
/**
* The remote service to communicate with.
*/
private final JUnitHostAsync junitHost = (JUnitHostAsync) GWT.create(JUnitHost.class);
/**
* Handles all {@link InitialResponse InitialResponses}.
*/
private final InitialResponseListener initialResponseListener = new InitialResponseListener();
/**
* Handles all {@link TestBlock TestBlocks}.
*/
private final TestBlockListener testBlockListener = new TestBlockListener();
/**
* The maximum number of times to retry communication with the server per
* test batch.
*/
private int maxRetryCount = -1;
/**
* If true, run a single test case with no RPC.
*/
private boolean serverless = false;
// TODO(FINDBUGS): can this be a private constructor to avoid multiple
// instances?
public GWTRunner() {
sInstance = this;
// Bind junitHost to the appropriate url.
ServiceDefTarget endpoint = (ServiceDefTarget) junitHost;
String url = GWT.getModuleBaseURL() + "junithost";
endpoint.setServiceEntryPoint(url);
// Null out the default uncaught exception handler since we will control it.
GWT.setUncaughtExceptionHandler(null);
}
public void onModuleLoad() {
clientInfo = new ClientInfo(parseQueryParamInteger(
SESSIONID_QUERY_PARAM, -1), getUserAgentProperty());
maxRetryCount = parseQueryParamInteger(RETRYCOUNT_QUERY_PARAM, -1);
currentBlock = checkForQueryParamTestToRun();
if (currentBlock != null) {
/*
* Just run a single test with no server-side interaction.
*/
serverless = true;
runTest();
} else {
/*
* Normal operation: Kick off the test running process by getting the
* first method to run from the server.
*/
syncToServer();
}
}
public void reportResultsAndGetNextMethod(JUnitResult result) {
if (serverless) {
// That's it, we're done
return;
}
if (result != null && failureMessage != null) {
RuntimeException ex = new RuntimeException(failureMessage);
result.setException(ex);
} else if (!GWT.isProdMode() && result.exceptionWrapper != null) {
SerializationStreamFactory fac = (SerializationStreamFactory) junitHost;
SerializationStreamWriter writer = fac.createStreamWriter();
ensureSerializable(result.exceptionWrapper, writer);
}
TestInfo currentTest = getCurrentTest();
currentResults.put(currentTest, result);
++currentTestIndex;
if (currentTestIndex < currentBlock.getTests().length) {
// Run the next test after a short delay.
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
public void execute() {
doRunTest();
}
});
} else {
syncToServer();
}
}
/**
* Implemented by the generated subclass. Creates an instance of the specified
* test class by fully qualified name.
*/
protected abstract GWTTestCase createNewTestCase(String testClass);
/**
* Implemented by the generated subclass. Get the value of the user agent
* property.
*/
protected abstract String getUserAgentProperty();
private TestBlock checkForQueryParamTestToRun() {
String testClass = Window.Location.getParameter(TESTCLASS_QUERY_PARAM);
String testMethod = Window.Location.getParameter(TESTFUNC_QUERY_PARAM);
if (testClass == null || testMethod == null) {
return null;
}
// TODO: support blocks of tests?
TestInfo[] tests = new TestInfo[] {new TestInfo(GWT.getModuleName(),
testClass, testMethod)};
return new TestBlock(tests, 0);
}
private void doRunTest() {
// Make sure the module matches.
String currentModule = GWT.getModuleName();
String newModule = getCurrentTest().getTestModule();
if (currentModule.equals(newModule)) {
// The module is correct.
runTest();
} else {
/*
* We're being asked to run a test in a different module. We must navigate
* the browser to a new URL which will run that other module. We retain
* the same path suffix (e.g., '/junit.html') as the current URL.
*/
String currentPath = Window.Location.getPath();
String pathSuffix = currentPath.substring(currentPath.lastIndexOf('/'));
UrlBuilder builder = Window.Location.createUrlBuilder();
builder.setParameter(BLOCKINDEX_QUERY_PARAM,
Integer.toString(currentBlock.getIndex())).setPath(
newModule + pathSuffix);
// Hand off the session id to the next module.
if (clientInfo.getSessionId() >= 0) {
builder.setParameter(SESSIONID_QUERY_PARAM,
String.valueOf(clientInfo.getSessionId()));
}
Window.Location.replace(builder.buildString());
currentBlock = null;
currentTestIndex = 0;
}
}
private TestInfo getCurrentTest() {
return currentBlock.getTests()[currentTestIndex];
}
/**
* Parse an integer from a query parameter, returning the default value if
* the parameter cannot be found.
*
* @param paramName the parameter name
* @param defaultValue the default value
* @return the integer value of the parameter
*/
private int parseQueryParamInteger(String paramName, int defaultValue) {
String value = Window.Location.getParameter(paramName);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
setFailureMessage("'" + value + "' is not a valid value for " +
paramName + ".");
return defaultValue;
}
}
return defaultValue;
}
private void runTest() {
// Dynamically create a new test case.
TestInfo currentTest = getCurrentTest();
GWTTestCase testCase = null;
Throwable caught = null;
try {
testCase = createNewTestCase(currentTest.getTestClass());
} catch (Throwable e) {
caught = e;
}
if (testCase == null) {
RuntimeException ex = new RuntimeException(currentTest
+ ": could not instantiate the requested class", caught);
JUnitResult result = new JUnitResult();
result.setException(ex);
reportResultsAndGetNextMethod(result);
return;
}
testCase.setName(currentTest.getTestMethod());
testCase.__doRunTest();
}
/**
* Fail all tests with the specified message.
*/
private void setFailureMessage(String message) {
failureMessage = message;
}
private void syncToServer() {
if (currentBlock == null) {
int firstBlockIndex = parseQueryParamInteger(BLOCKINDEX_QUERY_PARAM, 0);
junitHost.getTestBlock(firstBlockIndex, clientInfo,
initialResponseListener);
} else {
junitHost.reportResultsAndGetTestBlock(currentResults,
currentBlock.getIndex() + 1, clientInfo, testBlockListener);
}
}
}