| /* |
| * 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); |
| } |
| } |
| |
| } |