This patch adds "batch" execution of GWTTestCases, significantly reducing the
synchronization and network overhead.
Patch by: amitmanjhi
Review (and simplifications) by: scottb
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5766 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/common.ant.xml b/common.ant.xml
index 37cfcce..e6ccd23 100755
--- a/common.ant.xml
+++ b/common.ant.xml
@@ -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="" />
diff --git a/user/src/com/google/gwt/junit/BatchingStrategy.java b/user/src/com/google/gwt/junit/BatchingStrategy.java
new file mode 100644
index 0000000..c8b102d
--- /dev/null
+++ b/user/src/com/google/gwt/junit/BatchingStrategy.java
@@ -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};
+ }
+}
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index 169391c..4cec301 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -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;
+ }
}
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 2a11a2b..052ba3f 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -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 {
@@ -445,6 +477,11 @@
}
/**
+ * Determines how to batch up tests for execution.
+ */
+ private BatchingStrategy batchingStrategy = new NoBatchingStrategy();
+
+ /**
* When headless, all logging goes to the console.
*/
private PrintWriterTreeLogger consoleLogger;
@@ -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.
@@ -637,6 +676,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);
}
/**
diff --git a/user/src/com/google/gwt/junit/client/GWTTestCase.java b/user/src/com/google/gwt/junit/client/GWTTestCase.java
index 256e153..d2fe917 100644
--- a/user/src/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/src/com/google/gwt/junit/client/GWTTestCase.java
@@ -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.
*/
@@ -133,6 +145,22 @@
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
* normally, this test will not immediately succeed. Instead, a <i>delay
@@ -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();
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
index a968fd1..18bf8cf 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
@@ -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;
}
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
index b26a588..7c25d06 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
@@ -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);
}
diff --git a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
index 9e34312..f9c16e8 100644
--- a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
@@ -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);
}
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 5da715e..a249549 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -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);
}
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
index 2a679a9..363da9c 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
@@ -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;
+ /**
+ * The current block of tests to execute.
+ */
+ private TestInfo currentBlock[];
- private TestInfo currentTest;
+ /**
+ * 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);
}
}