| /* | |
| * 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.JUnitResult; | |
| 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 java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.IdentityHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| 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 userAgent, String desc) { | |
| super(sessionId, userAgent); | |
| 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; | |
| } | |
| } | |
| private static final Set<Class<? extends Throwable>> THROWABLES_NOT_RETRIED = createThrowablesNotRetried(); | |
| private static Set<Class<? extends Throwable>> createThrowablesNotRetried() { | |
| Set<Class<? extends Throwable>> throwableSet = new HashSet<Class<? extends Throwable>>(); | |
| throwableSet.add(com.google.gwt.junit.JUnitFatalLaunchException.class); | |
| throwableSet.add(java.lang.Error.class); | |
| return throwableSet; | |
| } | |
| /** | |
| * 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>>(); | |
| /** | |
| * A set of the GWT user agents (eg. ie6, gecko1_8) that have connected. | |
| */ | |
| private final Set<String> userAgents = new HashSet<String>(); | |
| /** | |
| * 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) { | |
| userAgents.add(clientInfo.getUserAgent()); | |
| 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"); | |
| } | |
| userAgents.add(clientInfo.getUserAgent()); | |
| 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 list of all user agents that have connected. | |
| */ | |
| String[] getUserAgents() { | |
| synchronized (clientStatusesLock) { | |
| return userAgents.toArray(new String[userAgents.size()]); | |
| } | |
| } | |
| /** | |
| * 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; | |
| } | |
| Throwable exception = result.getException(); | |
| if (exception != null && !isMember(exception, THROWABLES_NOT_RETRIED)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| 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; | |
| } | |
| private boolean isMember(Throwable exception, | |
| Set<Class<? extends Throwable>> throwableSet) { | |
| for (Class<? extends Throwable> throwable : throwableSet) { | |
| if (throwable.isInstance(exception)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } |