Revision: 5766 Author: amitman...@google.com Date: Tue Jul 21 15:13:53 2009 Log: This patch adds "batch" execution of GWTTestCases, significantly reducing the synchronization and network overhead.
Patch by: amitmanjhi Review (and simplifications) by: scottb http://code.google.com/p/google-web-toolkit/source/detail?r=5766 Added: /trunk/user/src/com/google/gwt/junit/BatchingStrategy.java Modified: /trunk/common.ant.xml /trunk/user/src/com/google/gwt/junit/JUnitMessageQueue.java /trunk/user/src/com/google/gwt/junit/JUnitShell.java /trunk/user/src/com/google/gwt/junit/client/GWTTestCase.java /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHost.java /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java /trunk/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java /trunk/user/src/com/google/gwt/junit/server/JUnitHostImpl.java /trunk/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java ======================================= --- /dev/null +++ /trunk/user/src/com/google/gwt/junit/BatchingStrategy.java Tue Jul 21 15:13:53 2009 @@ -0,0 +1,67 @@ +/* + * Copyright 2009 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; + +import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; + +import java.util.Set; + +/** + * An interface that specifies how tests should be batched. + */ +public interface BatchingStrategy { + + /** + * Returns the list of tests that should be executed along with this test. + */ + TestInfo[] getTestBlock(TestInfo currentTest); +} + +/** + * + * Strategy that does not batch tests. + */ +class NoBatchingStrategy implements BatchingStrategy { + public TestInfo[] getTestBlock(TestInfo currentTest) { + return new TestInfo[] {currentTest}; + } +} + +/** + * Strategy that batches all tests belonging to one module. + */ +class ModuleBatchingStrategy implements BatchingStrategy { + + /** + * Returns the list of all tests belonging to the module of + * <code>currentTest</code>. + */ + public TestInfo[] getTestBlock(TestInfo currentTest) { + String moduleName = currentTest.getTestModule(); + if (moduleName.endsWith(".JUnit")) { + moduleName = moduleName.substring(0, moduleName.length() + - ".JUnit".length()); + } + Set<TestInfo> allTestsInModule = GWTTestCase.ALL_GWT_TESTS.get(moduleName); + if (allTestsInModule != null) { + assert allTestsInModule.size() > 0; + return allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]); + } + // No data, default to just this test. + return new TestInfo[] {currentTest}; + } +} ======================================= --- /trunk/common.ant.xml Mon Jul 13 10:39:35 2009 +++ /trunk/common.ant.xml Tue Jul 21 15:13:53 2009 @@ -160,7 +160,7 @@ <macrodef name="gwt.junit"> <!-- TODO: make this more generic / refactor so it can be used from dev/core --> - <attribute name="test.args" default="" /> + <attribute name="test.args" default="-batch module" /> <attribute name="test.out" default="" /> <attribute name="test.reports" default="@{test.out}/reports" /> <attribute name="test.cases" default="" /> ======================================= --- /trunk/user/src/com/google/gwt/junit/JUnitMessageQueue.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/JUnitMessageQueue.java Tue Jul 21 15:13:53 2009 @@ -23,10 +23,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; /** - * A message queue to pass data between {...@link JUnitShell} and {...@link - * com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner. + * A message queue to pass data between {...@link JUnitShell} and + * {...@link com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner. * * <p> * The public methods are called by the servlet to find out what test to execute @@ -45,8 +46,10 @@ */ public static class ClientStatus { public final String clientId; - - public JUnitResult currentTestResults = null; + /** + * Stores the testResults for the current block of tests. + */ + public Map<TestInfo, JUnitResult> currentTestBlockResults = null; public boolean hasRequestedCurrentTest = false; public boolean isNew = true; @@ -68,7 +71,7 @@ /** * The current test to execute. */ - private TestInfo currentTest; + private TestInfo[] currentBlock; /** * The number of TestCase clients executing in parallel. @@ -88,14 +91,14 @@ } /** - * Called by the servlet to query for for the next method to test. + * Called by the servlet to query for for the next block to test. * * @param timeout how long to wait for an answer - * @return the next test to run, or <code>null</code> if - * <code>timeout</code> is exceeded or the next test does not match + * @return the next test to run, or <code>null</code> if <code>timeout</code> + * is exceeded or the next test does not match * <code>testClassName</code> */ - public TestInfo getNextTestInfo(String clientId, long timeout) + public TestInfo[] getNextTestBlock(String clientId, long timeout) throws TimeoutException { synchronized (clientStatusesLock) { ClientStatus clientStatus = clientStatuses.get(clientId); @@ -106,7 +109,7 @@ long startTime = System.currentTimeMillis(); long stopTime = startTime + timeout; - while (clientStatus.currentTestResults != null) { + while (clientStatus.currentTestBlockResults != null) { long timeToWait = stopTime - System.currentTimeMillis(); if (timeToWait < 1) { double elapsed = (System.currentTimeMillis() - startTime) / 1000.0; @@ -130,20 +133,27 @@ // Record that this client has retrieved the current test. clientStatus.hasRequestedCurrentTest = true; - return currentTest; + return currentBlock; } } + + public void reportFatalLaunch(String clientId, JUnitResult result) { + // Fatal launch error, cause this client to fail the whole block. + Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>(); + for (TestInfo testInfo : currentBlock) { + results.put(testInfo, result); + } + reportResults(clientId, results); + } /** * Called by the servlet to report the results of the last test to run. * - * @param testInfo the testInfo the result is for - * @param results the result of running the test + * @param results the result of running the test block */ - public void reportResults(String clientId, TestInfo testInfo, - JUnitResult results) { + public void reportResults(String clientId, Map<TestInfo, JUnitResult> results) { synchronized (clientStatusesLock) { - if (testInfo != null && !testInfo.equals(currentTest)) { + if (results != null && !resultsMatchCurrentBlock(results)) { // A client is reporting results for the wrong test. return; } @@ -157,7 +167,7 @@ clientStatus = new ClientStatus(clientId); clientStatuses.put(clientId, clientStatus); } - clientStatus.currentTestResults = results; + clientStatus.currentTestBlockResults = results; clientStatusesLock.notifyAll(); } } @@ -168,10 +178,10 @@ * @return Fetches a human-readable representation of the current test object */ String getCurrentTestName() { - if (currentTest == null) { + if (currentBlock == null) { return "(no test)"; } - return currentTest.toString(); + return currentBlock[0].toString(); } /** @@ -212,11 +222,24 @@ * * @return A map of results from all clients. */ - Map<String, JUnitResult> getResults() { + Map<TestInfo, Map<String, JUnitResult>> getResults() { synchronized (clientStatusesLock) { - Map<String, JUnitResult> result = new HashMap<String, JUnitResult>(); + /* + * All this overly complicated piece of code does is transform mappings + * keyed by clientId into mappings keyed by TestInfo. + */ + Map<TestInfo, Map<String, JUnitResult>> result = new HashMap<TestInfo, Map<String, JUnitResult>>(); for (ClientStatus clientStatus : clientStatuses.values()) { - result.put(clientStatus.clientId, clientStatus.currentTestResults); + for (Entry<TestInfo, JUnitResult> entry : clientStatus.currentTestBlockResults.entrySet()) { + TestInfo testInfo = entry.getKey(); + JUnitResult clientResultForThisTest = entry.getValue(); + Map<String, JUnitResult> targetMap = result.get(testInfo); + if (targetMap == null) { + targetMap = new HashMap<String, JUnitResult>(); + result.put(testInfo, targetMap); + } + targetMap.put(clientStatus.clientId, clientResultForThisTest); + } } return result; } @@ -272,7 +295,7 @@ int itemCount = 0; for (ClientStatus clientStatus : clientStatuses.values()) { if (clientStatus.hasRequestedCurrentTest - && clientStatus.currentTestResults == null) { + && clientStatus.currentTestBlockResults == null) { if (itemCount > 0) { buf.append(", "); } @@ -304,7 +327,7 @@ return false; } for (ClientStatus clientStatus : clientStatuses.values()) { - if (clientStatus.currentTestResults == null) { + if (clientStatus.currentTestBlockResults == null) { return false; } } @@ -315,12 +338,12 @@ /** * Called by the shell to set the next test to run. */ - void setNextTest(TestInfo testInfo) { + void setNextTestBlock(TestInfo[] testBlock) { synchronized (clientStatusesLock) { - this.currentTest = testInfo; + this.currentBlock = testBlock; for (ClientStatus clientStatus : clientStatuses.values()) { clientStatus.hasRequestedCurrentTest = false; - clientStatus.currentTestResults = null; + clientStatus.currentTestBlockResults = null; } clientStatusesLock.notifyAll(); } @@ -334,4 +357,14 @@ } } } -} + + private boolean resultsMatchCurrentBlock(Map<TestInfo, JUnitResult> results) { + assert results.size() == currentBlock.length; + for (TestInfo testInfo : currentBlock) { + if (!results.containsKey(testInfo)) { + return false; + } + } + return true; + } +} ======================================= --- /trunk/user/src/com/google/gwt/junit/JUnitShell.java Mon Jul 20 22:35:44 2009 +++ /trunk/user/src/com/google/gwt/junit/JUnitShell.java Tue Jul 21 15:13:53 2009 @@ -44,6 +44,7 @@ import junit.framework.TestResult; import java.util.ArrayList; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; @@ -66,10 +67,10 @@ * </p> * * <p> - * The client classes consist of the translatable version of {...@link - * com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, and the - * user's own {...@link com.google.gwt.junit.client.GWTTestCase}-derived class. - * The client communicates to the server via RPC. + * The client classes consist of the translatable version of + * {...@link com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, + * and the user's own {...@link com.google.gwt.junit.client.GWTTestCase}-derived + * class. The client communicates to the server via RPC. * </p> * * <p> @@ -238,6 +239,37 @@ } }); + // TODO: currently, only two values but soon may have multiple values. + registerHandler(new ArgHandlerString() { + @Override + public String getPurpose() { + return "Configure batch execution of tests"; + } + + @Override + public String getTag() { + return "-batch"; + } + + @Override + public String[] getTagArgs() { + return new String[] {"module"}; + } + + @Override + public boolean isUndocumented() { + return true; + } + + @Override + public boolean setString(String str) { + if (str.equals("module")) { + batchingStrategy = new ModuleBatchingStrategy(); + } + return true; + } + }); + registerHandler(new ArgHandler() { @Override public String[] getDefaultArgs() { @@ -337,8 +369,8 @@ /** * The amount of time to wait for all clients to complete a single test - * method, in milliseconds, measured from when the <i>last</i> client - * connects (and thus starts the test). 5 minutes. + * method, in milliseconds, measured from when the <i>last</i> client connects + * (and thus starts the test). 5 minutes. */ private static final long TEST_METHOD_TIMEOUT_MILLIS = 300000; @@ -365,8 +397,8 @@ /** * Entry point for {...@link com.google.gwt.junit.client.GWTTestCase}. Gets or - * creates the singleton {...@link JUnitShell} and invokes its {...@link - * #runTestImpl(String, TestCase, TestResult, Strategy)}. + * creates the singleton {...@link JUnitShell} and invokes its + * {...@link #runTestImpl(String, TestCase, TestResult, Strategy)}. */ public static void runTest(String moduleName, TestCase testCase, TestResult testResult) throws UnableToCompleteException { @@ -443,6 +475,11 @@ return unitTestShell; } + + /** + * Determines how to batch up tests for execution. + */ + private BatchingStrategy batchingStrategy = new NoBatchingStrategy(); /** * When headless, all logging goes to the console. @@ -510,6 +547,8 @@ */ private long testMethodTimeout; + private Map<TestInfo, Map<String, JUnitResult>> cachedResults = new HashMap<TestInfo, Map<String, JUnitResult>>(); + /** * Enforce the singleton pattern. The call to {...@link GWTShell}'s ctor forces * server mode and disables processing extra arguments as URLs to be shown. @@ -636,6 +675,49 @@ } super.compile(getTopLogger(), module); } + + private void processTestResult(TestInfo testInfo, TestCase testCase, + TestResult testResult, Strategy strategy) { + + Map<String, JUnitResult> results = cachedResults.get(testInfo); + assert results != null; + + boolean parallelTesting = numClients > 1; + + for (Entry<String, JUnitResult> entry : results.entrySet()) { + String clientId = entry.getKey(); + JUnitResult result = entry.getValue(); + assert (result != null); + Throwable exception = result.getException(); + // In the case that we're running multiple clients at once, we need to + // let the user know the browser in which the failure happened + if (parallelTesting && exception != null) { + String msg = "Remote test failed at " + clientId; + if (exception instanceof AssertionFailedError) { + AssertionFailedError newException = new AssertionFailedError(msg + + "\n" + exception.getMessage()); + newException.setStackTrace(exception.getStackTrace()); + newException.initCause(exception.getCause()); + exception = newException; + } else { + exception = new RuntimeException(msg, exception); + } + } + + // A "successful" failure. + if (exception instanceof AssertionFailedError) { + testResult.addFailure(testCase, (AssertionFailedError) exception); + } else if (exception != null) { + // A real failure + if (exception instanceof JUnitFatalLaunchException) { + lastLaunchFailed = true; + } + testResult.addError(testCase, exception); + } + + strategy.processResult(testCase, result); + } + } /** * Runs a particular test case. @@ -682,8 +764,19 @@ return; } - messageQueue.setNextTest(new TestInfo(currentModule.getName(), - testCase.getClass().getName(), testCase.getName())); + TestInfo testInfo = new TestInfo(currentModule.getName(), + testCase.getClass().getName(), testCase.getName()); + if (cachedResults.containsKey(testInfo)) { + // Already have a result. + processTestResult(testInfo, testCase, testResult, strategy); + return; + } + + /* + * Need to process test. Set up synchronization. + */ + TestInfo[] testBlock = batchingStrategy.getTestBlock(testInfo); + messageQueue.setNextTestBlock(testBlock); try { if (firstLaunch) { @@ -710,43 +803,9 @@ } assert (messageQueue.hasResult()); - Map<String, JUnitResult> results = messageQueue.getResults(); - - boolean parallelTesting = numClients > 1; - - for (Entry<String, JUnitResult> entry : results.entrySet()) { - String clientId = entry.getKey(); - JUnitResult result = entry.getValue(); - assert (result != null); - Throwable exception = result.getException(); - // In the case that we're running multiple clients at once, we need to - // let the user know the browser in which the failure happened - if (parallelTesting && exception != null) { - String msg = "Remote test failed at " + clientId; - if (exception instanceof AssertionFailedError) { - AssertionFailedError newException = new AssertionFailedError(msg - + "\n" + exception.getMessage()); - newException.setStackTrace(exception.getStackTrace()); - newException.initCause(exception.getCause()); - exception = newException; - } else { - exception = new RuntimeException(msg, exception); - } - } - - // A "successful" failure - if (exception instanceof AssertionFailedError) { - testResult.addFailure(testCase, (AssertionFailedError) exception); - } else if (exception != null) { - // A real failure - if (exception instanceof JUnitFatalLaunchException) { - lastLaunchFailed = true; - } - testResult.addError(testCase, exception); - } - - strategy.processResult(testCase, result); - } + cachedResults = messageQueue.getResults(); + assert cachedResults.containsKey(testInfo); + processTestResult(testInfo, testCase, testResult, strategy); } /** ======================================= --- /trunk/user/src/com/google/gwt/junit/client/GWTTestCase.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/client/GWTTestCase.java Tue Jul 21 15:13:53 2009 @@ -16,10 +16,16 @@ package com.google.gwt.junit.client; import com.google.gwt.junit.JUnitShell; +import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; import junit.framework.TestCase; import junit.framework.TestResult; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + /** * Acts as a bridge between the JUnit environment and the GWT environment. We * hook the run method and stash the TestResult object for later communication @@ -36,6 +42,12 @@ */ public abstract class GWTTestCase extends TestCase { + /** + * Records all live GWTTestCases by module name so we can optimize run they + * are compiled and run. + */ + public static final Map<String, Set<TestInfo>> ALL_GWT_TESTS = new HashMap<String, Set<TestInfo>>(); + /* * Object that collects the results of this test case execution. */ @@ -132,6 +144,22 @@ testResult = result; super.run(result); } + + @Override + public void setName(String name) { + super.setName(name); + + // Once the name is set, we can add ourselves to the global set. + String moduleName = getModuleName(); + Set<TestInfo> testsInThisModule = ALL_GWT_TESTS.get(moduleName); + if (testsInThisModule == null) { + // Preserve the order. + testsInThisModule = new LinkedHashSet<TestInfo>(); + ALL_GWT_TESTS.put(moduleName, testsInThisModule); + } + testsInThisModule.add(new TestInfo(moduleName + ".JUnit", + getClass().getName(), getName())); + } /** * Put the current test in asynchronous mode. If the test method completes @@ -218,8 +246,10 @@ @Override protected void runTest() throws Throwable { if (this.getName() == null) { - throw new IllegalArgumentException("GWTTestCases require a name; \"" + this.toString() - + "\" has none. Perhaps you used TestSuite.addTest() instead of addTestClass()?"); + throw new IllegalArgumentException( + "GWTTestCases require a name; \"" + + this.toString() + + "\" has none. Perhaps you used TestSuite.addTest() instead of addTestClass()?"); } String moduleName = getModuleName(); ======================================= --- /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHost.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHost.java Tue Jul 21 15:13:53 2009 @@ -19,6 +19,8 @@ import com.google.gwt.user.client.rpc.IsSerializable; import com.google.gwt.user.client.rpc.RemoteService; +import java.util.HashMap; + /** * An interface for {...@link com.google.gwt.junit.client.GWTTestCase} to * communicate with the test process through RPC. @@ -85,17 +87,16 @@ * @return the next test to run * @throws TimeoutException if the wait for the next method times out. */ - TestInfo getFirstMethod() throws TimeoutException; + TestInfo[] getFirstMethod() throws TimeoutException; /** * Reports results for the last method run and gets the name of next method to * run. * - * @param testInfo the testInfo the result is for - * @param result the results of executing the test + * @param results the results of executing the test * @return the next test to run * @throws TimeoutException if the wait for the next method times out. */ - TestInfo reportResultsAndGetNextMethod(TestInfo testInfo, JUnitResult result) - throws TimeoutException; -} + TestInfo[] reportResultsAndGetNextMethod( + HashMap<TestInfo, JUnitResult> results) throws TimeoutException; +} ======================================= --- /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java Tue Jul 21 15:13:53 2009 @@ -18,6 +18,8 @@ import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; import com.google.gwt.user.client.rpc.AsyncCallback; +import java.util.HashMap; + /** * The asynchronous version of {...@link JUnitHost}. */ @@ -29,17 +31,16 @@ * @param callBack the object that will receive the name of the next method to * run */ - void getFirstMethod(AsyncCallback<TestInfo> callBack); + void getFirstMethod(AsyncCallback<TestInfo[]> callBack); /** * Reports results for the last method run and gets the name of next method to * run. * - * @param testInfo the testInfo the result is for - * @param result the result of the test + * @param results the results of the tests * @param callBack the object that will receive the name of the next method to * run */ - void reportResultsAndGetNextMethod(TestInfo testInfo, JUnitResult result, - AsyncCallback<TestInfo> callBack); -} + void reportResultsAndGetNextMethod(HashMap<TestInfo, JUnitResult> results, + AsyncCallback<TestInfo[]> callBack); +} ======================================= --- /trunk/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java Tue Jul 21 15:13:53 2009 @@ -27,6 +27,7 @@ import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.junit.client.impl.GWTRunner; +import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; import com.google.gwt.user.rebind.SourceWriter; @@ -38,7 +39,6 @@ * This class generates a stub class for classes that derive from GWTTestCase. * This stub class provides the necessary bridge between our Hosted or Hybrid * mode classes and the JUnit system. - * */ public class GWTRunnerGenerator extends Generator { @@ -77,9 +77,8 @@ String moduleName; try { - ConfigurationProperty prop - = context.getPropertyOracle().getConfigurationProperty( - "junit.moduleName"); + ConfigurationProperty prop = context.getPropertyOracle().getConfigurationProperty( + "junit.moduleName"); moduleName = prop.getValues().get(0); } catch (BadPropertyValueException e) { logger.log(TreeLogger.ERROR, @@ -97,9 +96,20 @@ generatedClass, GWT_RUNNER_NAME); if (sourceWriter != null) { - JClassType[] allTestTypes = getAllPossibleTestTypes(context.getTypeOracle()); - Set<String> testClasses = getTestTypesForModule(logger, moduleName, - allTestTypes); + // Check the global set of active tests for this module. + Set<TestInfo> moduleTests = GWTTestCase.ALL_GWT_TESTS.get(moduleName); + Set<String> testClasses; + if (moduleTests == null || moduleTests.isEmpty()) { + // Fall back to pulling in all types in the module. + JClassType[] allTestTypes = getAllPossibleTestTypes(context.getTypeOracle()); + testClasses = getTestTypesForModule(logger, moduleName, allTestTypes); + } else { + // Must use sorted set to prevent nondeterminism. + testClasses = new TreeSet<String>(); + for (TestInfo testInfo : moduleTests) { + testClasses.add(testInfo.getTestClass()); + } + } writeCreateNewTestCaseMethod(testClasses, sourceWriter); sourceWriter.commit(logger); } ======================================= --- /trunk/user/src/com/google/gwt/junit/server/JUnitHostImpl.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/src/com/google/gwt/junit/server/JUnitHostImpl.java Tue Jul 21 15:13:53 2009 @@ -30,6 +30,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.util.HashMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -80,20 +81,22 @@ fld.set(obj, value); } - public TestInfo getFirstMethod() throws TimeoutException { - return getHost().getNextTestInfo(getClientId(getThreadLocalRequest()), + public TestInfo[] getFirstMethod() throws TimeoutException { + return getHost().getNextTestBlock(getClientId(getThreadLocalRequest()), TIME_TO_WAIT_FOR_TESTNAME); } - public TestInfo reportResultsAndGetNextMethod(TestInfo testInfo, - JUnitResult result) throws TimeoutException { - initResult(getThreadLocalRequest(), result); - ExceptionWrapper ew = result.getExceptionWrapper(); - result.setException(deserialize(ew)); + public TestInfo[] reportResultsAndGetNextMethod( + HashMap<TestInfo, JUnitResult> results) throws TimeoutException { + for (JUnitResult result : results.values()) { + initResult(getThreadLocalRequest(), result); + ExceptionWrapper ew = result.getExceptionWrapper(); + result.setException(deserialize(ew)); + } JUnitMessageQueue host = getHost(); String clientId = getClientId(getThreadLocalRequest()); - host.reportResults(clientId, testInfo, result); - return host.getNextTestInfo(clientId, TIME_TO_WAIT_FOR_TESTNAME); + host.reportResults(clientId, results); + return host.getNextTestBlock(clientId, TIME_TO_WAIT_FOR_TESTNAME); } @Override @@ -105,7 +108,7 @@ JUnitResult result = new JUnitResult(); initResult(request, result); result.setException(new JUnitFatalLaunchException(requestPayload)); - getHost().reportResults(getClientId(request), null, result); + getHost().reportFatalLaunch(getClientId(request), result); } else { super.service(request, response); } ======================================= --- /trunk/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java Mon Jul 13 10:39:35 2009 +++ /trunk/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java Tue Jul 21 15:13:53 2009 @@ -19,11 +19,15 @@ import com.google.gwt.core.client.GWT; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DeferredCommand; 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. * @@ -37,7 +41,7 @@ * The RPC callback object for {...@link GWTRunner#junitHost}. When * {...@link #onSuccess(Object)} is called, it's time to run the next test case. */ - private final class JUnitHostListener implements AsyncCallback<TestInfo> { + private final class JUnitHostListener implements AsyncCallback<TestInfo[]> { /** * A call to junitHost failed. @@ -55,9 +59,11 @@ /** * A call to junitHost succeeded; run the next test case. */ - public void onSuccess(TestInfo nextTest) { - currentTest = nextTest; - if (currentTest != null) { + public void onSuccess(TestInfo[] nextTestBlock) { + currentBlock = nextTestBlock; + currentBlockIndex = 0; + currentResults.clear(); + if (currentBlock != null && currentBlock.length > 0) { doRunTest(); } } @@ -82,9 +88,20 @@ return sInstance; } - private JUnitResult currentResult; - - private TestInfo currentTest; + /** + * The current block of tests to execute. + */ + private TestInfo currentBlock[]; + + /** + * Active test within current block of tests. + */ + private int currentBlockIndex = 0; + + /** + * Results for all test cases in the current block. + */ + private HashMap<TestInfo, JUnitResult> currentResults = new HashMap<TestInfo, JUnitResult>(); /** * The remote service to communicate with. @@ -116,8 +133,8 @@ } public void onModuleLoad() { - currentTest = checkForQueryParamTestToRun(); - if (currentTest != null) { + currentBlock = checkForQueryParamTestToRun(); + if (currentBlock != null) { /* * Just run a single test with no server-side interaction. */ @@ -137,8 +154,19 @@ // That's it, we're done return; } - currentResult = result; - syncToServer(); + TestInfo currentTest = getCurrentTest(); + currentResults.put(currentTest, result); + ++currentBlockIndex; + if (currentBlockIndex < currentBlock.length) { + // Run the next test after a short delay. + DeferredCommand.addCommand(new Command() { + public void execute() { + doRunTest(); + } + }); + } else { + syncToServer(); + } } /** @@ -147,19 +175,21 @@ */ protected abstract GWTTestCase createNewTestCase(String testClass); - private TestInfo checkForQueryParamTestToRun() { + private TestInfo[] checkForQueryParamTestToRun() { String testClass = Window.Location.getParameter(TESTCLASS_QUERY_PARAM); String testMethod = Window.Location.getParameter(TESTFUNC_QUERY_PARAM); if (testClass == null || testMethod == null) { return null; } - return new TestInfo(GWT.getModuleName(), testClass, testMethod); + // TODO: support blocks of tests? + return new TestInfo[] {new TestInfo(GWT.getModuleName(), testClass, + testMethod)}; } private void doRunTest() { // Make sure the module matches. String currentModule = GWT.getModuleName(); - String newModule = currentTest.getTestModule(); + String newModule = getCurrentTest().getTestModule(); if (currentModule.equals(newModule)) { // The module is correct. runTest(); @@ -171,11 +201,18 @@ String href = Window.Location.getHref(); String newHref = href.replace(currentModule, newModule); Window.Location.replace(newHref); + currentBlock = null; + currentBlockIndex = 0; } } + + private TestInfo getCurrentTest() { + return currentBlock[currentBlockIndex]; + } private void runTest() { // Dynamically create a new test case. + TestInfo currentTest = getCurrentTest(); GWTTestCase testCase = null; Throwable caught = null; try { @@ -197,11 +234,10 @@ } private void syncToServer() { - if (currentTest == null) { + if (currentBlock == null) { junitHost.getFirstMethod(junitHostListener); } else { - junitHost.reportResultsAndGetNextMethod(currentTest, currentResult, - junitHostListener); + junitHost.reportResultsAndGetNextMethod(currentResults, junitHostListener); } } --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---