blob: e3c449b0e87b2554680058bf63001b6b444f4d31 [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;
import com.google.gwt.junit.client.TimeoutException;
import com.google.gwt.junit.client.impl.JUnitHost.ClientInfo;
import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
import com.google.gwt.junit.client.impl.JUnitResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
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.
*
* <p>
* The public methods are called by the servlet to find out what test to execute
* next, and to report the results of the last test to run.
* </p>
*
* <p>
* The protected methods are called by the shell to fetch test results and drive
* the next test the client should run.
* </p>
*/
public class JUnitMessageQueue {
/**
* Server-side client info that includes a description.
*/
public static class ClientInfoExt extends ClientInfo {
/**
* A description of this client.
*/
private final String desc;
public ClientInfoExt(int sessionId, String desc) {
super(sessionId);
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
/**
* Holds the state of an individual client.
*/
public static class ClientStatus {
private int blockIndex = 0;
private ClientInfoExt clientInfo;
private boolean isNew = true;
public ClientStatus(ClientInfoExt clientInfo) {
this.clientInfo = clientInfo;
}
public String getDesc() {
return clientInfo.getDesc();
}
public int getId() {
return clientInfo.getSessionId();
}
@Override
public String toString() {
return clientInfo.getDesc();
}
public void updateClientInfo(ClientInfoExt clientInfo) {
this.clientInfo = clientInfo;
}
}
/**
* Records results for each client; must lock before accessing.
*/
private final Map<Integer, ClientStatus> clientStatuses = new HashMap<Integer, ClientStatus>();
/**
* The lock used to synchronize access to clientStatuses.
*/
private final Object clientStatusesLock = new Object();
/**
* Set to true when the last test block has been added. This is used to tell
* clients that all tests are complete.
*/
private boolean isLastTestBlockAvailable;
/**
* The number of TestCase clients executing in parallel.
*/
private int numClients = 1;
/**
* The list of test blocks to run.
*/
private final List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
/**
* Maps the TestInfo to the results from each clientId. If JUnitResult is
* null, it means that the client requested the test but did not report the
* results yet.
*/
private final Map<TestInfo, Map<ClientStatus, JUnitResult>> testResults = new HashMap<TestInfo, Map<ClientStatus, JUnitResult>>();
/**
* Only instantiable within this package.
*/
JUnitMessageQueue(int numClients) {
this.numClients = numClients;
}
/**
* Called by the servlet to query for for the next block to test.
*
* @param clientInfo information about the client
* @param blockIndex the index of the test block to get
* @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
* <code>testClassName</code>
*/
public TestBlock getTestBlock(ClientInfoExt clientInfo, int blockIndex,
long timeout) throws TimeoutException {
synchronized (clientStatusesLock) {
ClientStatus clientStatus = ensureClientStatus(clientInfo);
clientStatus.blockIndex = blockIndex;
// The client has finished all of the tests.
if (isLastTestBlockAvailable && blockIndex >= testBlocks.size()) {
return null;
}
long startTime = System.currentTimeMillis();
long stopTime = startTime + timeout;
while (blockIndex >= testBlocks.size()) {
long timeToWait = stopTime - System.currentTimeMillis();
if (timeToWait < 1) {
double elapsed = (System.currentTimeMillis() - startTime) / 1000.0;
throw new TimeoutException("The servlet did not respond to the "
+ "next query to test within " + timeout + "ms.\n"
+ " Client description: " + clientInfo.getDesc() + "\n"
+ " Actual time elapsed: " + elapsed + " seconds.\n");
}
try {
clientStatusesLock.wait(timeToWait);
} catch (InterruptedException e) {
/*
* Should never happen; but if it does, just send a null back to the
* client, which will cause it to stop running tests.
*/
System.err.println("Unexpected thread interruption");
e.printStackTrace();
return null;
}
}
// Record that this client has retrieved the current tests.
TestInfo[] tests = testBlocks.get(blockIndex);
for (TestInfo testInfo : tests) {
ensureResults(testInfo).put(clientStatus, null);
}
return new TestBlock(tests, blockIndex);
}
}
/**
* Reports a failure from a client that cannot startup.
*
* @param clientInfo information about the client
* @param result the failure result
*/
public void reportFatalLaunch(ClientInfoExt clientInfo, JUnitResult result) {
// Fatal launch error, cause this client to fail the whole block.
ClientStatus clientStatus = ensureClientStatus(clientInfo);
Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
for (TestInfo testInfo : testBlocks.get(clientStatus.blockIndex)) {
results.put(testInfo, result);
}
reportResults(clientInfo, results);
}
/**
* Called by the servlet to report the results of the last test to run.
*
* @param clientInfo information about the client
* @param results the result of running the test block
*/
public void reportResults(ClientInfoExt clientInfo,
Map<TestInfo, JUnitResult> results) {
synchronized (clientStatusesLock) {
if (results == null) {
throw new IllegalArgumentException("results cannot be null");
}
ClientStatus clientStatus = ensureClientStatus(clientInfo);
// Cache the test results.
for (Map.Entry<TestInfo, JUnitResult> entry : results.entrySet()) {
TestInfo testInfo = entry.getKey();
ensureResults(testInfo).put(clientStatus, entry.getValue());
}
clientStatusesLock.notifyAll();
}
}
/**
* Called by the shell to add test blocks to test.
*
* @param isLastBlock true if this is the last test block that will be added
*/
void addTestBlocks(List<TestInfo[]> newTestBlocks, boolean isLastBlock) {
synchronized (clientStatusesLock) {
if (isLastTestBlockAvailable) {
throw new IllegalArgumentException(
"Cannot add test blocks after the last block is added");
}
for (TestInfo[] testBlock : newTestBlocks) {
if (testBlock.length == 0) {
throw new IllegalArgumentException("TestBlocks cannot be empty");
}
}
testBlocks.addAll(newTestBlocks);
if (isLastBlock) {
isLastTestBlockAvailable = true;
}
clientStatusesLock.notifyAll();
}
}
/**
* Returns any new clients that have contacted the server since the last call.
* The same client will never be returned from this method twice.
*/
String[] getNewClients() {
synchronized (clientStatusesLock) {
List<String> results = new ArrayList<String>();
for (ClientStatus clientStatus : clientStatuses.values()) {
if (clientStatus.isNew) {
results.add(clientStatus.getDesc());
// Record that this client is no longer new.
clientStatus.isNew = false;
}
}
clientStatusesLock.notifyAll();
return results.toArray(new String[results.size()]);
}
}
int getNumClients() {
return numClients;
}
/**
* Returns how many clients have requested the currently-running test.
*
* @param testInfo the {@link TestInfo} that the clients retrieved
*/
int getNumClientsRetrievedTest(TestInfo testInfo) {
synchronized (clientStatusesLock) {
int count = 0;
Map<ClientStatus, JUnitResult> results = testResults.get(testInfo);
if (results != null) {
count = results.size();
}
return count;
}
}
/**
* Returns how many clients have connected.
*/
int getNumConnectedClients() {
synchronized (clientStatusesLock) {
return clientStatuses.size();
}
}
/**
* Fetches the results of a completed test.
*
* @param testInfo the {@link TestInfo} to check for results
* @return A map of results from all clients.
*/
Map<ClientStatus, JUnitResult> getResults(TestInfo testInfo) {
synchronized (clientStatusesLock) {
return testResults.get(testInfo);
}
}
/**
* Visible for testing.
*
* @return the test blocks
*/
List<TestInfo[]> getTestBlocks() {
return testBlocks;
}
/**
* Returns a pretty printed list of clients that have not retrieved the
* current test. Used for error reporting.
*
* @param testInfo the {@link TestInfo} we are waiting for
* @return a string containing the list of clients that have not retrieved the
* current test.
*/
String getUnretrievedClients(TestInfo testInfo) {
synchronized (clientStatusesLock) {
Map<ClientStatus, JUnitResult> results = testResults.get(testInfo);
StringBuilder buf = new StringBuilder();
int lineCount = 0;
for (ClientStatus clientStatus : clientStatuses.values()) {
if (lineCount > 0) {
buf.append('\n');
}
if (results == null || !results.containsKey(clientStatus)) {
buf.append(" - NO RESPONSE: ");
} else {
buf.append(" - (ok): ");
}
buf.append(clientStatus.getDesc());
++lineCount;
}
int difference = numClients - getNumClientsRetrievedTest(testInfo);
if (difference > 0) {
if (lineCount > 0) {
buf.append('\n');
}
buf.append(" - "
+ difference
+ " client(s) haven't responded back to JUnitShell since the start of the test.");
}
return buf.toString();
}
}
/**
* Returns a human-formatted message identifying what clients have connected
* but have not yet reported results for this test. It is used in a timeout
* condition, to identify what we're still waiting on.
*
* @param testInfo the {@link TestInfo} that the clients are working on
* @return human readable message
*/
String getWorkingClients(TestInfo testInfo) {
synchronized (clientStatusesLock) {
// Print a list of clients that have connected but not returned results.
int itemCount = 0;
StringBuilder buf = new StringBuilder();
Map<ClientStatus, JUnitResult> results = testResults.get(testInfo);
if (results != null) {
for (Map.Entry<ClientStatus, JUnitResult> entry : results.entrySet()) {
if (entry.getValue() == null) {
buf.append(entry.getKey().getDesc());
buf.append("\n");
itemCount++;
}
}
}
// Print the number of other clients.
int difference = numClients - itemCount;
if (difference > 0) {
if (itemCount > 0) {
buf.append('\n');
}
buf.append(difference
+ " other client(s) haven't responded back to JUnitShell since the start of the test.");
}
return buf.toString();
}
}
/**
* Called by the shell to see if the currently-running test has completed.
*
* @param testInfo the {@link TestInfo} to check for results
* @return If the test has completed, <code>true</code>, otherwise
* <code>false</code>.
*/
boolean hasResults(TestInfo testInfo) {
synchronized (clientStatusesLock) {
Map<ClientStatus, JUnitResult> results = testResults.get(testInfo);
if (results == null || results.size() < numClients) {
return false;
}
for (JUnitResult result : results.values()) {
if (result == null) {
return false;
}
}
return true;
}
}
/**
* Returns true iff any there are no results, missing results, or any of the
* test results is an exception other than those in {@code
* THROWABLES_NOT_RETRIED}.
*/
boolean needsRerunning(TestInfo testInfo) {
Map<ClientStatus, JUnitResult> results = getResults(testInfo);
if (results == null) {
return true;
}
if (results.size() != numClients) {
return true;
}
for (Entry<ClientStatus, JUnitResult> entry : results.entrySet()) {
JUnitResult result = entry.getValue();
if (result == null) {
return true;
}
if (isNonFatalFailure(result)) {
return true;
}
}
return false;
}
private boolean isNonFatalFailure(JUnitResult result) {
return result.isAnyException()
&& !result.isExceptionOf(Error.class)
&& !result.isExceptionOf(JUnitFatalLaunchException.class);
}
void removeResults(TestInfo testInfo) {
synchronized (clientStatusesLock) {
testResults.remove(testInfo);
}
}
void waitForResults(int millis) {
synchronized (clientStatusesLock) {
try {
clientStatusesLock.wait(millis);
} catch (InterruptedException e) {
}
}
}
/**
* Ensure that a {@link ClientStatus} for the clientId exists.
*
* @param clientId the id of the client
* @return the {@link ClientStatus} for the client
*/
private ClientStatus ensureClientStatus(ClientInfoExt clientInfo) {
int id = clientInfo.getSessionId();
ClientStatus clientStatus = clientStatuses.get(id);
if (clientStatus == null) {
clientStatus = new ClientStatus(clientInfo);
clientStatuses.put(id, clientStatus);
} else {
// Maybe update the client info (IP might change if through a proxy).
clientStatus.updateClientInfo(clientInfo);
}
return clientStatus;
}
/**
* Get the map of test results from all clients for a given {@link TestInfo},
* creating it if necessary.
*
* @param testInfo the {@link TestInfo}
* @return the map of all results
*/
private Map<ClientStatus, JUnitResult> ensureResults(TestInfo testInfo) {
Map<ClientStatus, JUnitResult> results = testResults.get(testInfo);
if (results == null) {
results = new IdentityHashMap<ClientStatus, JUnitResult>();
testResults.put(testInfo, results);
}
return results;
}
}