/* * 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.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 class GWTRunner implements EntryPoint { private final class InitialResponseListener implements AsyncCallback<InitialResponse> { /** * Delegate to the {@link TestBlockListener}. */ @Override public void onFailure(Throwable caught) { testBlockListener.onFailure(caught); } /** * Update our client info with the server-provided session id then delegate * to the {@link TestBlockListener}. */ @Override public void onSuccess(InitialResponse result) { clientInfo = new ClientInfo(result.getSessionId()); 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. */ @Override public void onFailure(Throwable caught) { if (curRetryCount++ < MAX_RETRY_COUNT) { reportWarning("Retrying syncing back to junit backend. (Exception: " + caught + ")"); // Try the call again new Timer() { @Override public void run() { syncToServer(); } }.schedule(1000); } else { reportFatalError("Cannot sync back to junit backend: " + caught); } } /** * A call to junitHost succeeded; run the next test case. */ @Override public void onSuccess(TestBlock nextTestBlock) { curRetryCount = 0; currentBlock = nextTestBlock; currentTestIndex = 0; currentResults.clear(); if (currentBlock != null && currentBlock.getTests().length > 0) { doRunTest(); } } } /** * The singleton instance. */ static GWTRunner sInstance; /** * A query param specifying my unique session cookie. */ private static final String SESSIONID_QUERY_PARAM = "gwt.junit.sessionId"; /** * The maximum number of times to retry communication with the server per test batch. */ private static final int MAX_RETRY_COUNT = 3; /** * 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; } /** * 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(); private GWTTestAccessor testAccessor; // 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); } @Override public void onModuleLoad() { testAccessor = new GWTTestAccessor(); clientInfo = new ClientInfo(parseQueryParamInteger(SESSIONID_QUERY_PARAM, -1)); // Kick off the test running process by getting the first method to run from the server. syncToServer(); } public void reportResultsAndGetNextMethod(JUnitResult result) { if (failureMessage != null) { RuntimeException ex = new RuntimeException(failureMessage); result.setException(ex); } 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() { @Override public void execute() { doRunTest(); } }); } else { syncToServer(); } } /** * Executes a test on provided test class instance. */ public void executeTestMethod(GWTTestCase testCase, String className, String methodName) throws Throwable { testAccessor.invoke(testCase, className, methodName); } 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())); } // Replace "%3A" with ":" as a hack to support broken DevMode plugins. Window.Location.replace(builder.buildString().replaceAll("%3A", ":")); 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; try { testCase = testAccessor.newInstance(currentTest.getTestClass()); } catch (Throwable e) { RuntimeException ex = new RuntimeException(currentTest + ": could not instantiate the requested class", e); JUnitResult result = new JUnitResult(); result.setException(ex); reportResultsAndGetNextMethod(result); return; } testCase.init(currentTest.getTestClass(), 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); } } private static native void reportFatalError(String errorMsg)/*-{ $wnd.junitError("/fatal", errorMsg); }-*/; private static native void reportWarning(String errorMsg)/*-{ $wnd.junitError("", errorMsg); }-*/; }