| /* |
| * 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.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.core.ext.TreeLogger.Type; |
| import com.google.gwt.dev.BootStrapPlatform; |
| import com.google.gwt.dev.GWTShell; |
| import com.google.gwt.dev.cfg.ModuleDef; |
| import com.google.gwt.dev.cfg.ModuleDefLoader; |
| import com.google.gwt.dev.cfg.Properties; |
| import com.google.gwt.dev.cfg.Property; |
| import com.google.gwt.dev.shell.BrowserWidgetHost; |
| import com.google.gwt.dev.util.log.PrintWriterTreeLogger; |
| import com.google.gwt.junit.client.TimeoutException; |
| import com.google.gwt.junit.client.impl.GWTRunner; |
| import com.google.gwt.junit.client.impl.JUnitResult; |
| import com.google.gwt.junit.remote.BrowserManager; |
| import com.google.gwt.util.tools.ArgHandlerFlag; |
| import com.google.gwt.util.tools.ArgHandlerString; |
| |
| import junit.framework.AssertionFailedError; |
| import junit.framework.TestCase; |
| import junit.framework.TestResult; |
| |
| import java.rmi.Naming; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * This class is responsible for hosting JUnit test case execution. There are |
| * three main pieces to the JUnit system. |
| * |
| * <ul> |
| * <li>Test environment</li> |
| * <li>Client classes</li> |
| * <li>Server classes</li> |
| * </ul> |
| * |
| * <p> |
| * The test environment consists of this class and the non-translatable version |
| * of {@link com.google.gwt.junit.client.GWTTestCase}. These two classes |
| * integrate directly into the real JUnit test process. |
| * </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. |
| * </p> |
| * |
| * <p> |
| * The server consists of {@link com.google.gwt.junit.server.JUnitHostImpl}, an |
| * RPC servlet which communicates back to the test environment through a |
| * {@link JUnitMessageQueue}, thus closing the loop. |
| * </p> |
| */ |
| public class JUnitShell extends GWTShell { |
| |
| /** |
| * A strategy for running the test. |
| */ |
| public interface Strategy { |
| String getModuleInherit(); |
| |
| String getSyntheticModuleExtension(); |
| |
| void processResult(TestCase testCase, JUnitResult result); |
| } |
| |
| private static class JUnitStrategy implements Strategy { |
| public String getModuleInherit() { |
| return "com.google.gwt.junit.JUnit"; |
| } |
| |
| public String getSyntheticModuleExtension() { |
| return "JUnit"; |
| } |
| |
| public void processResult(TestCase testCase, JUnitResult result) { |
| } |
| } |
| |
| /** |
| * This is a system property that, when set, emulates command line arguments. |
| */ |
| private static final String PROP_GWT_ARGS = "gwt.args"; |
| |
| /** |
| * This legacy system property, when set, causes us to run in web mode. |
| * (Superceded by passing "-web" into gwt.args). |
| */ |
| private static final String PROP_JUNIT_HYBRID_MODE = "gwt.hybrid"; |
| |
| /** |
| * The amount of time to wait for all clients to have contacted the server and |
| * begun running the test. |
| */ |
| private static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000; |
| |
| /** |
| * Singleton object for hosting unit tests. All test case instances executed |
| * by the TestRunner will use the single unitTestShell. |
| */ |
| private static JUnitShell unitTestShell; |
| |
| /** |
| * Called by {@link com.google.gwt.junit.server.JUnitHostImpl} to get an |
| * interface into the test process. |
| * |
| * @return The {@link JUnitMessageQueue} interface that belongs to the |
| * singleton {@link JUnitShell}, or <code>null</code> if no such |
| * singleton exists. |
| */ |
| public static JUnitMessageQueue getMessageQueue() { |
| if (unitTestShell == null) { |
| return null; |
| } |
| return unitTestShell.messageQueue; |
| } |
| |
| /** |
| * 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)}. |
| */ |
| public static void runTest(String moduleName, TestCase testCase, |
| TestResult testResult) throws UnableToCompleteException { |
| getUnitTestShell().runTestImpl(moduleName, testCase, testResult, |
| new JUnitStrategy()); |
| } |
| |
| public static void runTest(String moduleName, TestCase testCase, |
| TestResult testResult, Strategy strategy) |
| throws UnableToCompleteException { |
| getUnitTestShell().runTestImpl(moduleName, testCase, testResult, strategy); |
| } |
| |
| /** |
| * Retrieves the JUnitShell. This should only be invoked during TestRunner |
| * execution of JUnit tests. |
| */ |
| private static JUnitShell getUnitTestShell() { |
| if (unitTestShell == null) { |
| BootStrapPlatform.go(); |
| JUnitShell shell = new JUnitShell(); |
| String[] args = shell.synthesizeArgs(); |
| if (!shell.processArgs(args)) { |
| throw new RuntimeException("Invalid shell arguments"); |
| } |
| |
| shell.messageQueue = new JUnitMessageQueue(shell.numClients); |
| |
| if (!shell.startUp()) { |
| throw new RuntimeException("Shell failed to start"); |
| } |
| unitTestShell = shell; |
| } |
| |
| return unitTestShell; |
| } |
| |
| /** |
| * When headless, all logging goes to the console. |
| */ |
| private PrintWriterTreeLogger consoleLogger; |
| |
| /** |
| * Name of the module containing the current/last module to run. |
| */ |
| private String currentModuleName; |
| |
| /** |
| * If true, the last attempt to launch failed. |
| */ |
| private boolean lastLaunchFailed; |
| |
| /** |
| * Portal to interact with the servlet. |
| */ |
| private JUnitMessageQueue messageQueue; |
| |
| /** |
| * The number of test clients executing in parallel. With -remoteweb, users |
| * can specify a number of parallel test clients, but by default we only have |
| * 1. |
| */ |
| private int numClients = 1; |
| |
| /** |
| * What type of test we're running; Local hosted, local web, or remote web. |
| */ |
| private RunStyle runStyle = new RunStyleLocalHosted(this); |
| |
| /** |
| * The time at which the current test will fail if the client has not yet |
| * started the test. |
| */ |
| private long testBeginTimeout; |
| |
| /** |
| * Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces |
| * server mode and disables processing extra arguments as URLs to be shown. |
| */ |
| private JUnitShell() { |
| super(true, true); |
| |
| registerHandler(new ArgHandlerFlag() { |
| |
| @Override |
| public String getPurpose() { |
| return "Causes your test to run in web (compiled) mode (defaults to hosted mode)"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-web"; |
| } |
| |
| @Override |
| public boolean setFlag() { |
| runStyle = new RunStyleLocalWeb(JUnitShell.this); |
| return true; |
| } |
| |
| }); |
| |
| registerHandler(new ArgHandlerString() { |
| |
| @Override |
| public String getPurpose() { |
| return "Runs web mode via RMI to a BrowserManagerServer; e.g. rmi://localhost/ie6"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-remoteweb"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[] {"rmiUrl"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return true; |
| } |
| |
| @Override |
| public boolean setString(String str) { |
| try { |
| String[] urls = str.split(","); |
| numClients = urls.length; |
| BrowserManager[] browserManagers = new BrowserManager[numClients]; |
| for (int i = 0; i < numClients; ++i) { |
| browserManagers[i] = (BrowserManager) Naming.lookup(urls[i]); |
| } |
| runStyle = new RunStyleRemoteWeb(JUnitShell.this, browserManagers); |
| } catch (Exception e) { |
| System.err.println("Error connecting to browser manager at " + str); |
| e.printStackTrace(); |
| System.exit(1); |
| return false; |
| } |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerFlag() { |
| |
| @Override |
| public String getPurpose() { |
| return "Causes the log window and browser windows to be displayed; useful for debugging"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-notHeadless"; |
| } |
| |
| @Override |
| public boolean setFlag() { |
| setHeadless(false); |
| return true; |
| } |
| }); |
| |
| setRunTomcat(true); |
| setHeadless(true); |
| |
| // Legacy: -Dgwt.hybrid runs web mode |
| if (System.getProperty(PROP_JUNIT_HYBRID_MODE) != null) { |
| runStyle = new RunStyleLocalWeb(this); |
| } |
| } |
| |
| @Override |
| public TreeLogger getTopLogger() { |
| if (consoleLogger != null) { |
| return consoleLogger; |
| } else { |
| return super.getTopLogger(); |
| } |
| } |
| |
| @Override |
| protected Type doGetDefaultLogLevel() { |
| return Type.WARN; |
| } |
| |
| /** |
| * Never check for updates in JUnit mode. |
| */ |
| @Override |
| protected boolean doShouldCheckForUpdates() { |
| return false; |
| } |
| |
| @Override |
| protected ArgHandlerPort getArgHandlerPort() { |
| return new ArgHandlerPort() { |
| @Override |
| public String[] getDefaultArgs() { |
| return new String[] {"-port", "auto"}; |
| } |
| }; |
| } |
| |
| @Override |
| protected void initializeLogger() { |
| if (isHeadless()) { |
| consoleLogger = new PrintWriterTreeLogger(); |
| consoleLogger.setMaxDetail(getLogLevel()); |
| } else { |
| super.initializeLogger(); |
| } |
| } |
| |
| /** |
| * Overrides {@link GWTShell#notDone()} to wait for the currently-running test |
| * to complete. |
| */ |
| @Override |
| protected boolean notDone() { |
| if (!messageQueue.haveAllClientsRetrievedCurrentTest() |
| && testBeginTimeout < System.currentTimeMillis()) { |
| throw new TimeoutException( |
| "The browser did not contact the server within " |
| + TEST_BEGIN_TIMEOUT_MILLIS + "ms."); |
| } |
| |
| if (messageQueue.hasResult(currentModuleName)) { |
| return false; |
| } |
| |
| return !runStyle.wasInterrupted(); |
| } |
| |
| void compileForWebMode(String moduleName, String userAgentString) |
| throws UnableToCompleteException { |
| ModuleDef module = doLoadModule(getTopLogger(), moduleName); |
| if (userAgentString != null) { |
| Properties props = module.getProperties(); |
| Property userAgent = props.find("user.agent"); |
| if (userAgent != null) { |
| userAgent.setActiveValue(userAgentString); |
| } |
| } |
| BrowserWidgetHost browserHost = getBrowserHost(); |
| assert (browserHost != null); |
| browserHost.compile(module); |
| } |
| |
| /** |
| * Runs a particular test case. |
| */ |
| private void runTestImpl(String moduleName, TestCase testCase, |
| TestResult testResult, Strategy strategy) |
| throws UnableToCompleteException { |
| |
| String syntheticModuleName = moduleName + "." |
| + strategy.getSyntheticModuleExtension(); |
| boolean sameTest = syntheticModuleName.equals(currentModuleName); |
| if (sameTest && lastLaunchFailed) { |
| throw new UnableToCompleteException(); |
| } |
| currentModuleName = syntheticModuleName; |
| |
| if (!sameTest) { |
| /* |
| * Synthesize a synthetic module that derives from the user-specified |
| * module but also includes JUnit support. |
| */ |
| ModuleDef synthetic = ModuleDefLoader.createSyntheticModule( |
| getTopLogger(), currentModuleName, new String[] { |
| moduleName, strategy.getModuleInherit()}, true); |
| // Replace any user entry points with our test runner. |
| synthetic.clearEntryPoints(); |
| synthetic.addEntryPointTypeName(GWTRunner.class.getName()); |
| // Squirrel away the name of the active module for GWTRunnerGenerator |
| Property moduleNameProp = synthetic.getProperties().create( |
| "junit.moduleName"); |
| moduleNameProp.addKnownValue(moduleName); |
| moduleNameProp.setActiveValue(moduleName); |
| } |
| |
| lastLaunchFailed = false; |
| messageQueue.setNextTestName(currentModuleName, |
| testCase.getClass().getName(), testCase.getName()); |
| |
| try { |
| runStyle.maybeLaunchModule(currentModuleName, !sameTest); |
| } catch (UnableToCompleteException e) { |
| lastLaunchFailed = true; |
| testResult.addError(testCase, new JUnitFatalLaunchException(e)); |
| return; |
| } |
| |
| // Wait for test to complete |
| try { |
| // Set a timeout period to automatically fail if the servlet hasn't been |
| // contacted; something probably went wrong (the module failed to load?) |
| testBeginTimeout = System.currentTimeMillis() + TEST_BEGIN_TIMEOUT_MILLIS; |
| pumpEventLoop(); |
| } catch (TimeoutException e) { |
| lastLaunchFailed = true; |
| testResult.addError(testCase, e); |
| return; |
| } |
| |
| List<JUnitResult> results = messageQueue.getResults(currentModuleName); |
| |
| if (results == null) { |
| return; |
| } |
| |
| boolean parallelTesting = numClients > 1; |
| |
| for (JUnitResult result : results) { |
| 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 " + result.getHost() + " on " |
| + result.getAgent(); |
| if (exception instanceof AssertionFailedError) { |
| AssertionFailedError newException = new AssertionFailedError(msg |
| + "\n" + exception.getMessage()); |
| newException.setStackTrace(exception.getStackTrace()); |
| 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); |
| } |
| } |
| |
| /** |
| * Synthesize command line arguments from a system property. |
| */ |
| private String[] synthesizeArgs() { |
| ArrayList<String> argList = new ArrayList<String>(); |
| |
| String args = System.getProperty(PROP_GWT_ARGS); |
| if (args != null) { |
| // Match either a non-whitespace, non start of quoted string, or a |
| // quoted string that can have embedded, escaped quoting characters |
| // |
| Pattern pattern = Pattern.compile("[^\\s\"]+|\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\""); |
| Matcher matcher = pattern.matcher(args); |
| while (matcher.find()) { |
| argList.add(matcher.group()); |
| } |
| } |
| |
| return argList.toArray(new String[argList.size()]); |
| } |
| } |