| /* |
| * 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.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.dev.ArgProcessorBase; |
| import com.google.gwt.dev.Compiler; |
| import com.google.gwt.dev.DevMode; |
| import com.google.gwt.dev.cfg.BindingProperty; |
| import com.google.gwt.dev.cfg.ModuleDef; |
| import com.google.gwt.dev.cfg.Properties; |
| import com.google.gwt.dev.cfg.Property; |
| import com.google.gwt.dev.javac.CompilationState; |
| import com.google.gwt.dev.javac.CompilationUnit; |
| import com.google.gwt.dev.shell.CheckForUpdates; |
| import com.google.gwt.dev.shell.jetty.JettyLauncher; |
| import com.google.gwt.dev.util.arg.ArgHandlerDisableAggressiveOptimization; |
| import com.google.gwt.dev.util.arg.ArgHandlerDisableCastChecking; |
| import com.google.gwt.dev.util.arg.ArgHandlerDisableClassMetadata; |
| import com.google.gwt.dev.util.arg.ArgHandlerDisableRunAsync; |
| import com.google.gwt.dev.util.arg.ArgHandlerDraftCompile; |
| import com.google.gwt.dev.util.arg.ArgHandlerEnableAssertions; |
| import com.google.gwt.dev.util.arg.ArgHandlerExtraDir; |
| import com.google.gwt.dev.util.arg.ArgHandlerGenDir; |
| import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers; |
| import com.google.gwt.dev.util.arg.ArgHandlerLogLevel; |
| import com.google.gwt.dev.util.arg.ArgHandlerMaxPermsPerPrecompile; |
| import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle; |
| import com.google.gwt.dev.util.arg.ArgHandlerWarDir; |
| import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional; |
| import com.google.gwt.junit.JUnitMessageQueue.ClientStatus; |
| import com.google.gwt.junit.client.GWTTestCase; |
| import com.google.gwt.junit.client.TimeoutException; |
| import com.google.gwt.junit.client.impl.JUnitResult; |
| import com.google.gwt.junit.client.impl.JUnitHost.TestInfo; |
| import com.google.gwt.util.tools.ArgHandlerFlag; |
| import com.google.gwt.util.tools.ArgHandlerInt; |
| import com.google.gwt.util.tools.ArgHandlerString; |
| |
| import junit.framework.AssertionFailedError; |
| import junit.framework.TestCase; |
| import junit.framework.TestResult; |
| |
| import org.mortbay.jetty.Server; |
| import org.mortbay.jetty.webapp.WebAppContext; |
| |
| import java.io.File; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Map.Entry; |
| 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 DevMode { |
| |
| /** |
| * A strategy for running the test. |
| */ |
| public interface Strategy { |
| String getModuleInherit(); |
| |
| String getSyntheticModuleExtension(); |
| |
| void processModule(ModuleDef module); |
| |
| void processResult(TestCase testCase, JUnitResult result); |
| } |
| |
| class ArgProcessor extends ArgProcessorBase { |
| |
| @SuppressWarnings("deprecation") |
| public ArgProcessor() { |
| /* |
| * ----- Options from DevModeBase ------- |
| */ |
| // DISABLE: ArgHandlerNoServerFlag. |
| registerHandler(new ArgHandlerPort(options) { |
| @Override |
| public String[] getDefaultArgs() { |
| // Override port to auto by default. |
| return new String[]{"-port", "auto"}; |
| } |
| }); |
| registerHandler(new ArgHandlerWhitelist()); |
| registerHandler(new ArgHandlerBlacklist()); |
| registerHandler(new ArgHandlerLogDir(options)); |
| registerHandler(new ArgHandlerLogLevel(options)); |
| registerHandler(new ArgHandlerGenDir(options)); |
| // DISABLE: ArgHandlerBindAddress. |
| registerHandler(new ArgHandlerCodeServerPort(options) { |
| @Override |
| public String[] getDefaultArgs() { |
| // Override code server port to auto by default. |
| return new String[]{this.getTag(), "auto"}; |
| } |
| }); |
| // DISABLE: ArgHandlerRemoteUI. |
| |
| /* |
| * ----- Options from DevMode ------- |
| */ |
| // Hard code the server. |
| options.setServletContainerLauncher(new MyJettyLauncher()); |
| // DISABLE: ArgHandlerStartupURLs |
| registerHandler(new com.google.gwt.dev.ArgHandlerOutDirDeprecated(options)); |
| registerHandler(new ArgHandlerWarDir(options) { |
| @Override |
| public String[] getDefaultArgs() { |
| // If an outDir was already specified, don't clobber it. |
| if (options.getOutDir() != null) { |
| return null; |
| } |
| return super.getDefaultArgs(); |
| } |
| }); |
| registerHandler(new ArgHandlerExtraDir(options)); |
| registerHandler(new ArgHandlerWorkDirOptional(options)); |
| // DISABLE: ArgHandlerModuleName |
| |
| /* |
| * ----- Additional options from Compiler not already included ------- |
| */ |
| registerHandler(new ArgHandlerScriptStyle(options)); |
| registerHandler(new ArgHandlerEnableAssertions(options)); |
| registerHandler(new ArgHandlerDisableAggressiveOptimization(options)); |
| registerHandler(new ArgHandlerDisableClassMetadata(options)); |
| registerHandler(new ArgHandlerDisableCastChecking(options)); |
| registerHandler(new ArgHandlerDisableRunAsync(options)); |
| registerHandler(new ArgHandlerDraftCompile(options)); |
| registerHandler(new ArgHandlerMaxPermsPerPrecompile(options)); |
| registerHandler(new ArgHandlerLocalWorkers(options)); |
| |
| /* |
| * ----- Options specific to JUnitShell ----- |
| */ |
| |
| // Override log level to set WARN by default.. |
| registerHandler(new ArgHandlerLogLevel(options) { |
| @Override |
| protected Type getDefaultLogLevel() { |
| return TreeLogger.WARN; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerFlag() { |
| @Override |
| public String getPurpose() { |
| return "Synonym for -prod (deprecated)"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-web"; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return true; |
| } |
| |
| @Override |
| public boolean setFlag() { |
| developmentMode = false; |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerFlag() { |
| @Override |
| public String getPurpose() { |
| return "Causes your test to run in production (compiled) mode (defaults to development mode)"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-prod"; |
| } |
| |
| @Override |
| public boolean setFlag() { |
| developmentMode = false; |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerInt() { |
| @Override |
| public String[] getDefaultArgs() { |
| return new String[]{getTag(), "5"}; |
| } |
| |
| @Override |
| public String getPurpose() { |
| return "Set the test method timeout, in minutes"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-testMethodTimeout"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"minutes"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return false; |
| } |
| |
| @Override |
| public void setInt(int minutes) { |
| baseTestMethodTimeoutMillis = minutes * 60 * 1000; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerInt() { |
| @Override |
| public String[] getDefaultArgs() { |
| return new String[]{getTag(), "1"}; |
| } |
| |
| @Override |
| public String getPurpose() { |
| return "Set the test begin timeout (time for clients to contact " |
| + "server), in minutes"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-testBeginTimeout"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"minutes"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return false; |
| } |
| |
| @Override |
| public void setInt(int minutes) { |
| baseTestBeginTimeoutMillis = minutes * 60 * 1000; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerString() { |
| @Override |
| public String getPurpose() { |
| return "Selects the runstyle to use for this test. The name is " |
| + "a suffix of com.google.gwt.junit.RunStyle or is a fully " |
| + "qualified class name, and may be followed with a colon and " |
| + "an argument for this runstyle. The specified class must" |
| + "extend RunStyle."; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-runStyle"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"runstyle[:args]"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return false; |
| } |
| |
| @Override |
| public boolean setString(String runStyleArg) { |
| runStyleName = runStyleArg; |
| return true; |
| } |
| }); |
| |
| 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[]{"none|class|module"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return true; |
| } |
| |
| @Override |
| public boolean setString(String str) { |
| if (str.equals("none")) { |
| batchingStrategy = new NoBatchingStrategy(); |
| } else if (str.equals("class")) { |
| batchingStrategy = new ClassBatchingStrategy(); |
| } else if (str.equals("module")) { |
| batchingStrategy = new ModuleBatchingStrategy(); |
| } else { |
| 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() { |
| JUnitShell.this.setHeadless(false); |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerString() { |
| @Override |
| public String getPurpose() { |
| return "Precompile modules as tests are running (speeds up remote tests but requires more memory)"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-precompile"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"simple|all|parallel"}; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return true; |
| } |
| |
| @Override |
| public boolean setString(String str) { |
| if (str.equals("simple")) { |
| compileStrategy = new SimpleCompileStrategy(JUnitShell.this); |
| } else if (str.equals("all")) { |
| compileStrategy = new PreCompileStrategy(JUnitShell.this); |
| } else if (str.equals("parallel")) { |
| compileStrategy = new ParallelCompileStrategy(JUnitShell.this); |
| } else { |
| return false; |
| } |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerFlag() { |
| @Override |
| public String getPurpose() { |
| return "Use CSS standards mode (rather than quirks mode) for the hosting page"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-standardsMode"; |
| } |
| |
| @Override |
| public boolean setFlag() { |
| setStandardsMode(true); |
| return true; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerInt() { |
| @Override |
| public String getPurpose() { |
| return "EXPERIMENTAL: Sets the maximum number of attempts for running each test method"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-Xtries"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"1"}; |
| } |
| |
| @Override |
| public boolean isRequired() { |
| return false; |
| } |
| |
| @Override |
| public boolean isUndocumented() { |
| return false; |
| } |
| |
| @Override |
| public void setInt(int value) { |
| tries = value; |
| } |
| }); |
| |
| registerHandler(new ArgHandlerString() { |
| @Override |
| public String getPurpose() { |
| return "Specify the user agents to reduce the number of permutations for remote browser tests;" |
| + " e.g. ie6,ie8,safari,gecko,gecko1_8,opera"; |
| } |
| |
| @Override |
| public String getTag() { |
| return "-userAgents"; |
| } |
| |
| @Override |
| public String[] getTagArgs() { |
| return new String[]{"userAgents"}; |
| } |
| |
| @Override |
| public boolean setString(String str) { |
| remoteUserAgents = str.split(","); |
| for (int i = 0; i < remoteUserAgents.length; i++) { |
| remoteUserAgents[i] = remoteUserAgents[i].trim(); |
| } |
| return true; |
| } |
| }); |
| } |
| |
| @Override |
| protected String getName() { |
| return JUnitShell.class.getName(); |
| } |
| } |
| |
| private final class MyJettyLauncher extends JettyLauncher { |
| |
| /** |
| * Adds in special JUnit stuff. |
| */ |
| @Override |
| protected JettyServletContainer createServletContainer(TreeLogger logger, |
| File appRootDir, Server server, WebAppContext wac, int localPort) { |
| // Don't bother shutting down cleanly. |
| server.setStopAtShutdown(false); |
| // Save off the Context so we can add our own servlets later. |
| JUnitShell.this.wac = wac; |
| return super.createServletContainer(logger, appRootDir, server, wac, |
| localPort); |
| } |
| |
| /** |
| * Ignore DevMode's normal WEB-INF classloader rules and just allow the |
| * system classloader to dominate. This makes JUnitHostImpl live in the |
| * right classloader (mine). |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| protected WebAppContext createWebAppContext(TreeLogger logger, |
| File appRootDir) { |
| return new WebAppContext(appRootDir.getAbsolutePath(), "/") { |
| { |
| // Prevent file locking on Windows; pick up file changes. |
| getInitParams().put( |
| "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false"); |
| |
| // Prefer the parent class loader so that JUnit works. |
| setParentLoaderPriority(true); |
| } |
| }; |
| } |
| } |
| |
| /** |
| * This is a system property that, when set, emulates command line arguments. |
| */ |
| private static final String PROP_GWT_ARGS = "gwt.args"; |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Get the list of remote user agents to compile. This method returns null |
| * until all clients have connected. |
| * |
| * @return the list of remote user agents |
| */ |
| public static String[] getRemoteUserAgents() { |
| if (unitTestShell == null) { |
| return null; |
| } |
| return unitTestShell.remoteUserAgents; |
| } |
| |
| /** |
| * Checks if a testCase should not be executed. Currently, a test is either |
| * executed on all clients (mentioned in this test) or on no clients. |
| * |
| * @param testInfo the test info to check |
| * @return true iff the test should not be executed on any of the specified |
| * clients. |
| */ |
| public static boolean mustNotExecuteTest(TestInfo testInfo) { |
| if (unitTestShell == null) { |
| throw new IllegalStateException( |
| "mustNotExecuteTest cannot be called before runTest()"); |
| } |
| try { |
| Class<?> testClass = TestCase.class.getClassLoader().loadClass( |
| testInfo.getTestClass()); |
| return unitTestShell.mustNotExecuteTest(getBannedPlatforms(testClass, |
| testInfo.getTestMethod())); |
| } catch (ClassNotFoundException e) { |
| throw new IllegalArgumentException("Could not load test class: " |
| + testInfo.getTestClass()); |
| } |
| } |
| |
| /** |
| * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or |
| * creates the singleton {@link JUnitShell} and invokes its |
| * {@link #runTestImpl(GWTTestCase, TestResult)}. |
| */ |
| public static void runTest(GWTTestCase testCase, TestResult testResult) |
| throws UnableToCompleteException { |
| getUnitTestShell().runTestImpl(testCase, testResult); |
| } |
| |
| /** |
| * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or |
| * creates the singleton {@link JUnitShell} and invokes its |
| * {@link #runTestImpl(GWTTestCase, TestResult)}. |
| * |
| * @deprecated use {@link #runTest(GWTTestCase, TestResult)} instead |
| */ |
| @Deprecated |
| public static void runTest(String moduleName, TestCase testCase, |
| TestResult testResult) throws UnableToCompleteException { |
| runTest(moduleName, testCase, testResult, |
| ((GWTTestCase) testCase).getStrategy()); |
| } |
| |
| /** |
| * @deprecated use {@link #runTest(GWTTestCase, TestResult)} instead |
| */ |
| @Deprecated |
| public static void runTest(String moduleName, TestCase testCase, |
| TestResult testResult, Strategy strategy) |
| throws UnableToCompleteException { |
| GWTTestCase gwtTestCase = (GWTTestCase) testCase; |
| assert moduleName != null : "moduleName cannot be null"; |
| assert strategy != null : "strategy cannot be null"; |
| assert moduleName.equals(gwtTestCase.getModuleName()) : "moduleName does not match GWTTestCase#getModuleName()"; |
| assert strategy.equals(gwtTestCase.getStrategy()) : "strategy does not match GWTTestCase#getStrategy()"; |
| runTest(gwtTestCase, testResult); |
| } |
| |
| /** |
| * Retrieves the JUnitShell. This should only be invoked during TestRunner |
| * execution of JUnit tests. |
| */ |
| static JUnitShell getUnitTestShell() { |
| if (unitTestShell == null) { |
| unitTestShell = new JUnitShell(); |
| unitTestShell.lastLaunchFailed = true; |
| String[] args = unitTestShell.synthesizeArgs(); |
| ArgProcessor argProcessor = unitTestShell.new ArgProcessor(); |
| if (!argProcessor.processArgs(args)) { |
| throw new JUnitFatalLaunchException("Error processing shell arguments"); |
| } |
| // Always bind to the wildcard address and substitute the host address in |
| // URLs. Note that connectAddress isn't actually used here, as we |
| // override it from the runsStyle in getModuleUrl, but we set it to match |
| // what will actually be used anyway to avoid confusion. |
| unitTestShell.options.setBindAddress("0.0.0.0"); |
| try { |
| unitTestShell.options.setConnectAddress(InetAddress.getLocalHost().getHostAddress()); |
| } catch (UnknownHostException e) { |
| throw new JUnitFatalLaunchException("Unable to resolve my address"); |
| } |
| if (!unitTestShell.startUp()) { |
| throw new JUnitFatalLaunchException("Shell failed to start"); |
| } |
| // TODO: install a shutdown hook? Not necessary with GWTShell. |
| unitTestShell.lastLaunchFailed = false; |
| } |
| unitTestShell.checkArgs(); |
| return unitTestShell; |
| } |
| |
| /** |
| * Sanity check; if the type we're trying to run did not actually wind up in |
| * the type oracle, there's no way this test can possibly run. Bail early |
| * instead of failing on the client. |
| */ |
| private static JUnitFatalLaunchException checkTestClassInCurrentModule( |
| CompilationState compilationState, String moduleName, TestCase testCase) { |
| TypeOracle typeOracle = compilationState.getTypeOracle(); |
| String typeName = testCase.getClass().getName(); |
| typeName = typeName.replace('$', '.'); |
| JClassType foundType = typeOracle.findType(typeName); |
| if (foundType != null) { |
| return null; |
| } |
| Map<String, CompilationUnit> unitMap = compilationState.getCompilationUnitMap(); |
| CompilationUnit unit = unitMap.get(typeName); |
| String errMsg; |
| if (unit == null) { |
| errMsg = "The test class '" + typeName + "' was not found in module '" |
| + moduleName + "'; no compilation unit for that type was seen"; |
| } else if (unit.isError()) { |
| errMsg = "The test class '" + typeName |
| + "' had compile errors; check log for details"; |
| } else if (!unit.isCompiled()) { |
| errMsg = "The test class '" |
| + typeName |
| + "' depends on a unit that had compile errors; check log for details"; |
| } else { |
| errMsg = "Unexpected error: the test class '" |
| + typeName |
| + "' appears to be valid, but no corresponding type was found in TypeOracle; please contact GWT support"; |
| } |
| return new JUnitFatalLaunchException(errMsg); |
| } |
| |
| /** |
| * Returns the set of banned {@code Platform} for a test method. |
| * |
| * @param testClass the testClass |
| * @param methodName the name of the test method |
| */ |
| private static Set<Platform> getBannedPlatforms(Class<?> testClass, |
| String methodName) { |
| Set<Platform> bannedSet = EnumSet.noneOf(Platform.class); |
| if (testClass.isAnnotationPresent(DoNotRunWith.class)) { |
| bannedSet.addAll(Arrays.asList(testClass.getAnnotation(DoNotRunWith.class).value())); |
| } |
| try { |
| Method testMethod = testClass.getMethod(methodName); |
| if (testMethod.isAnnotationPresent(DoNotRunWith.class)) { |
| bannedSet.addAll(Arrays.asList(testMethod.getAnnotation( |
| DoNotRunWith.class).value())); |
| } |
| } catch (SecurityException e) { |
| // should not happen |
| e.printStackTrace(); |
| } catch (NoSuchMethodException e) { |
| // should not happen |
| e.printStackTrace(); |
| } |
| return bannedSet; |
| } |
| |
| /** |
| * Our server's web app context; used to dynamically add servlets. |
| */ |
| WebAppContext wac; |
| |
| /** |
| * The amount of time to wait for all clients to have contacted the server and |
| * begin running the test. "Contacted" does not necessarily mean "the test has |
| * begun," e.g. for linker errors stopping the test initialization. |
| */ |
| private long baseTestBeginTimeoutMillis; |
| |
| /** |
| * 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). Set by the -testMethodTimeout argument. |
| */ |
| private long baseTestMethodTimeoutMillis; |
| |
| /** |
| * Determines how to batch up tests for execution. |
| */ |
| private BatchingStrategy batchingStrategy = new NoBatchingStrategy(); |
| |
| /** |
| * Determines how modules are compiled. |
| */ |
| private CompileStrategy compileStrategy = new SimpleCompileStrategy( |
| JUnitShell.this); |
| |
| /** |
| * A type oracle for the current module, used to validate class existence. |
| */ |
| private CompilationState currentCompilationState; |
| |
| /** |
| * Name of the module containing the current/last module to run. |
| */ |
| private ModuleDef currentModule; |
| |
| /** |
| * The name of the current test case being run. |
| */ |
| private TestInfo currentTestInfo; |
| |
| /** |
| * True if we are running the test in development mode. |
| */ |
| private boolean developmentMode = true; |
| |
| /** |
| * If true, no launches have yet been successful. |
| */ |
| private boolean firstLaunch = true; |
| |
| /** |
| * If true, the last attempt to launch failed. |
| */ |
| private boolean lastLaunchFailed; |
| |
| /** |
| * We need to keep a hard reference to the last module that was launched until |
| * all client browsers have successfully transitioned to the current module. |
| * Failure to do so allows the last module to be GC'd, which transitively |
| * kills the {@link com.google.gwt.junit.server.JUnitHostImpl} servlet. If the |
| * servlet dies, the client browsers will be unable to transition. |
| */ |
| @SuppressWarnings("unused") |
| private ModuleDef lastModule; |
| |
| /** |
| * Records what servlets have been loaded at which paths. |
| */ |
| private final Map<String, String> loadedServletsByPath = new HashMap<String, String>(); |
| |
| /** |
| * Portal to interact with the servlet. |
| */ |
| private JUnitMessageQueue messageQueue; |
| |
| /** |
| * An exception that should by fired the next time runTestImpl runs. |
| */ |
| private UnableToCompleteException pendingException; |
| |
| /** |
| * The remote user agents that have connected. Populated after all user agents |
| * have connected so we can limit permutations for remote tests. |
| */ |
| private String[] remoteUserAgents; |
| |
| /** |
| * What type of test we're running; Local development, local production, or |
| * remote production. |
| */ |
| private RunStyle runStyle = null; |
| |
| /** |
| * The argument passed to -runStyle. This is parsed later so we can pass in a |
| * logger. |
| */ |
| private String runStyleName = "HtmlUnit"; |
| |
| private boolean standardsMode = false; |
| |
| /** |
| * Test method timeout as modified by the batching strategy. |
| */ |
| private long testBatchingMethodTimeoutMillis; |
| |
| /** |
| * The time the test actually began. |
| */ |
| private long testBeginTime; |
| |
| /** |
| * The time at which the current test will fail if the client has not yet |
| * started the test. |
| */ |
| private long testBeginTimeout; |
| |
| /** |
| * Timeout for individual test method. If System.currentTimeMillis() is later |
| * than this timestamp, then we need to pack up and go home. Zero for "not yet |
| * set" (at the start of a test). This interval begins when the |
| * testBeginTimeout interval is done. |
| */ |
| private long testMethodTimeout; |
| |
| /** |
| * Max number of times a test method must be tried. |
| */ |
| private int tries; |
| |
| /** |
| * 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() { |
| setRunTomcat(true); |
| setHeadless(true); |
| } |
| |
| public String getModuleUrl(String moduleName) { |
| // TODO(jat): consider using DevModeBase.processUrl instead |
| String localhost = runStyle.getLocalHostName(); |
| String url = "http://" + localhost + ":" + getPort() + "/" + moduleName |
| + (standardsMode ? "/junit-standards.html" : "/junit.html"); |
| if (developmentMode) { |
| // CHECKSTYLE_OFF |
| url += "?gwt.codesvr=" + localhost + ":" + codeServerPort; |
| // CHECKSTYLE_ON |
| } |
| return url; |
| } |
| |
| /** |
| * Check for updates once a minute. |
| */ |
| @Override |
| protected long checkForUpdatesInterval() { |
| return CheckForUpdates.ONE_MINUTE; |
| } |
| |
| @Override |
| protected boolean doStartup() { |
| if (!super.doStartup()) { |
| return false; |
| } |
| int numClients = createRunStyle(runStyleName); |
| if (numClients < 0) { |
| // RunStyle already logged reasons for its failure |
| return false; |
| } |
| messageQueue = new JUnitMessageQueue(numClients); |
| |
| if (tries >= 1) { |
| runStyle.setTries(tries); |
| } |
| |
| if (!runStyle.setupMode(getTopLogger(), developmentMode)) { |
| getTopLogger().log( |
| TreeLogger.ERROR, |
| "Run style does not support " |
| + (developmentMode ? "development" : "production") + " mode"); |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| protected void ensureCodeServerListener() { |
| if (developmentMode) { |
| super.ensureCodeServerListener(); |
| listener.setIgnoreRemoteDeath(true); |
| } |
| } |
| |
| @Override |
| protected ModuleDef loadModule(TreeLogger logger, String moduleName, |
| boolean refresh) throws UnableToCompleteException { |
| // Never refresh modules in JUnit. |
| return super.loadModule(logger, moduleName, false); |
| } |
| |
| /** |
| * Checks to see if this test run is complete. |
| */ |
| protected boolean notDone() { |
| int activeClients = messageQueue.getNumClientsRetrievedTest(currentTestInfo); |
| int expectedClients = messageQueue.getNumClients(); |
| if (firstLaunch && runStyle instanceof RunStyleManual) { |
| String[] newClients = messageQueue.getNewClients(); |
| int printIndex = activeClients - newClients.length + 1; |
| for (String newClient : newClients) { |
| System.out.println(printIndex + " - " + newClient); |
| ++printIndex; |
| } |
| if (activeClients != expectedClients) { |
| // Wait forever for first contact; user-driven. |
| return true; |
| } |
| } |
| |
| // Limit permutations after all clients have connected. |
| if (remoteUserAgents == null |
| && messageQueue.getNumConnectedClients() == expectedClients) { |
| remoteUserAgents = messageQueue.getUserAgents(); |
| String userAgentList = ""; |
| for (int i = 0; i < remoteUserAgents.length; i++) { |
| if (i > 0) { |
| userAgentList += ", "; |
| } |
| userAgentList += remoteUserAgents[i]; |
| } |
| getTopLogger().log( |
| TreeLogger.INFO, |
| "All clients connected (Limiting future permutations to: " |
| + userAgentList + ")"); |
| } |
| |
| long currentTimeMillis = System.currentTimeMillis(); |
| if (activeClients >= expectedClients) { |
| if (activeClients > expectedClients) { |
| getTopLogger().log( |
| TreeLogger.WARN, |
| "Too many clients: expected " + expectedClients + ", found " |
| + activeClients); |
| } |
| firstLaunch = false; |
| |
| /* |
| * It's now safe to release any reference to the last module since all |
| * clients have transitioned to the current module. |
| */ |
| lastModule = currentModule; |
| if (testMethodTimeout == 0) { |
| testMethodTimeout = currentTimeMillis + testBatchingMethodTimeoutMillis; |
| } else if (testMethodTimeout < currentTimeMillis) { |
| double elapsed = (currentTimeMillis - testBeginTime) / 1000.0; |
| throw new TimeoutException( |
| "The browser did not complete the test method " |
| + currentTestInfo.toString() + " in " |
| + testBatchingMethodTimeoutMillis |
| + "ms.\n We have no results from:\n" |
| + messageQueue.getWorkingClients(currentTestInfo) |
| + "Actual time elapsed: " + elapsed + " seconds.\n"); |
| } |
| } else if (testBeginTimeout < currentTimeMillis) { |
| double elapsed = (currentTimeMillis - testBeginTime) / 1000.0; |
| throw new TimeoutException( |
| "The browser did not contact the server within " |
| + baseTestBeginTimeoutMillis + "ms.\n" |
| + messageQueue.getUnretrievedClients(currentTestInfo) |
| + "\n Actual time elapsed: " + elapsed + " seconds.\n"); |
| } |
| |
| // Check that we haven't lost communication with a remote host. |
| String[] interruptedHosts = runStyle.getInterruptedHosts(); |
| if (interruptedHosts != null) { |
| StringBuilder msg = new StringBuilder(); |
| msg.append("A remote browser died a mysterious death.\n"); |
| msg.append(" We lost communication with:"); |
| for (String host : interruptedHosts) { |
| msg.append("\n ").append(host); |
| } |
| throw new TimeoutException(msg.toString()); |
| } |
| |
| if (messageQueue.hasResults(currentTestInfo)) { |
| return false; |
| } else if (pendingException == null) { |
| // Instead of waiting around for results, try to compile the next module. |
| try { |
| compileStrategy.maybeCompileAhead(); |
| } catch (UnableToCompleteException e) { |
| pendingException = e; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| protected void warnAboutNoStartupUrls() { |
| // do nothing -- JUnitShell isn't expected to have startup URLs |
| } |
| |
| void compileForWebMode(ModuleDef module, String... userAgents) |
| throws UnableToCompleteException { |
| if (userAgents != null && userAgents.length > 0) { |
| Properties props = module.getProperties(); |
| Property userAgent = props.find("user.agent"); |
| if (userAgent instanceof BindingProperty) { |
| BindingProperty bindingProperty = (BindingProperty) userAgent; |
| bindingProperty.setAllowedValues(bindingProperty.getRootCondition(), |
| userAgents); |
| } |
| } |
| if (!new Compiler(options).run(getTopLogger(), module)) { |
| throw new UnableToCompleteException(); |
| } |
| // TODO(scottb): prepopulate currentCompilationState somehow? |
| } |
| |
| void maybeCompileForWebMode(ModuleDef module, String... userAgents) |
| throws UnableToCompleteException { |
| // Load any declared servlets. |
| for (String path : module.getServletPaths()) { |
| String servletClass = module.findServletForPath(path); |
| path = '/' + module.getName() + path; |
| if (!servletClass.equals(loadedServletsByPath.get(path))) { |
| try { |
| Class<?> clazz = wac.loadClass(servletClass); |
| wac.addServlet(clazz, path); |
| loadedServletsByPath.put(path, servletClass); |
| } catch (ClassNotFoundException e) { |
| getTopLogger().log( |
| TreeLogger.WARN, |
| "Failed to load servlet class '" + servletClass |
| + "' declared in '" + module.getName() + "'", e); |
| } |
| } |
| } |
| if (developmentMode) { |
| // BACKWARDS COMPATIBILITY: most linkers currently fail in dev mode. |
| if (module.getLinker("std") != null) { |
| // TODO: unfortunately, this could be race condition between dev/prod |
| module.addLinker("std"); |
| } |
| super.link(getTopLogger(), module); |
| } else { |
| compileForWebMode(module, userAgents); |
| } |
| } |
| |
| /** |
| * @deprecated TODO(fabbott) delete me |
| */ |
| @Deprecated |
| @SuppressWarnings("unused") |
| void setNumClients(int numClients) { |
| throw new UnsupportedOperationException("This method should be deleted."); |
| } |
| |
| void setStandardsMode(boolean standardsMode) { |
| this.standardsMode = standardsMode; |
| } |
| |
| private void checkArgs() { |
| if (runStyle.getTries() > 1 |
| && !(batchingStrategy instanceof NoBatchingStrategy)) { |
| throw new JUnitFatalLaunchException( |
| "Batching does not work with tries > 1"); |
| } |
| } |
| |
| /** |
| * Create the specified (or default) runStyle. |
| * |
| * @param runStyleName the argument passed to -runStyle |
| * @return the number of clients, or -1 if initialization was unsuccessful |
| */ |
| private int createRunStyle(String runStyleName) { |
| String args = null; |
| String name = runStyleName; |
| int colon = name.indexOf(':'); |
| if (colon >= 0) { |
| args = name.substring(colon + 1); |
| name = name.substring(0, colon); |
| } |
| if (name.indexOf('.') < 0) { |
| name = RunStyle.class.getName() + name; |
| } |
| Throwable caught = null; |
| try { |
| Class<?> clazz = Class.forName(name); |
| Class<? extends RunStyle> runStyleClass = clazz.asSubclass(RunStyle.class); |
| Constructor<? extends RunStyle> ctor = runStyleClass.getConstructor(JUnitShell.class); |
| runStyle = ctor.newInstance(JUnitShell.this); |
| return runStyle.initialize(args); |
| } catch (ClassNotFoundException e) { |
| // special error message for CNFE since it is likely a typo |
| String msg = "Unable to create runStyle \"" + runStyleName + "\""; |
| if (runStyleName.indexOf('.') < 0 && runStyleName.length() > 0 |
| && Character.isLowerCase(runStyleName.charAt(0))) { |
| // apparently using a built-in runstyle with an initial lowercase letter |
| msg += " - did you mean \"" |
| + Character.toUpperCase(runStyleName.charAt(0)) |
| + runStyleName.substring(1) + "\"?"; |
| } else { |
| msg += " -- is it spelled correctly?"; |
| } |
| getTopLogger().log(TreeLogger.ERROR, msg); |
| return -1; |
| } catch (SecurityException e) { |
| caught = e; |
| } catch (NoSuchMethodException e) { |
| caught = e; |
| } catch (IllegalArgumentException e) { |
| caught = e; |
| } catch (InstantiationException e) { |
| caught = e; |
| } catch (IllegalAccessException e) { |
| caught = e; |
| } catch (InvocationTargetException e) { |
| caught = e; |
| } |
| getTopLogger().log(TreeLogger.ERROR, |
| "Unable to create runStyle \"" + runStyleName + "\"", caught); |
| return -1; |
| } |
| |
| private boolean mustNotExecuteTest(Set<Platform> bannedPlatforms) { |
| // TODO (amitmanjhi): Remove this hard-coding. A RunStyle somehow needs to |
| // specify how it interacts with the platforms. |
| if (runStyle instanceof RunStyleHtmlUnit |
| && (bannedPlatforms.contains(Platform.HtmlUnitUnknown) |
| || bannedPlatforms.contains(Platform.HtmlUnitLayout) || bannedPlatforms.contains(Platform.HtmlUnitBug))) { |
| return true; |
| } |
| |
| if (developmentMode) { |
| if (bannedPlatforms.contains(Platform.Devel)) { |
| return true; |
| } |
| } else { |
| // Prod mode |
| if (bannedPlatforms.contains(Platform.Prod)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private boolean mustRetry(int numTries) { |
| if (numTries >= runStyle.getTries()) { |
| return false; |
| } |
| assert (batchingStrategy instanceof NoBatchingStrategy); |
| // checked in {@code checkArgs()} |
| /* |
| * If a batching strategy is being used, the client will already have moved |
| * passed the failed test case. The whole block could be re-executed, but it |
| * would be more complicated. |
| */ |
| return true; |
| } |
| |
| private void processTestResult(TestCase testCase, TestResult testResult, |
| Strategy strategy) { |
| |
| Map<ClientStatus, JUnitResult> results = messageQueue.getResults(currentTestInfo); |
| assert results != null; |
| assert results.size() == messageQueue.getNumClients() : results.size() |
| + " != " + messageQueue.getNumClients(); |
| |
| for (Entry<ClientStatus, JUnitResult> entry : results.entrySet()) { |
| ClientStatus client = entry.getKey(); |
| JUnitResult result = entry.getValue(); |
| assert (result != null); |
| Throwable exception = result.getException(); |
| |
| // Let the user know the browser in which the failure happened. |
| if (exception != null) { |
| String msg = "Remote test failed at " + client.getDesc(); |
| if (exception instanceof AssertionFailedError) { |
| String oldMessage = exception.getMessage(); |
| if (oldMessage != null) { |
| msg += "\n" + exception.getMessage(); |
| } |
| AssertionFailedError newException = new AssertionFailedError(msg); |
| 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); |
| } |
| } |
| |
| private void runTestImpl(GWTTestCase testCase, TestResult testResult) |
| throws UnableToCompleteException { |
| runTestImpl(testCase, testResult, 0); |
| } |
| |
| /** |
| * Runs a particular test case. |
| */ |
| private void runTestImpl(GWTTestCase testCase, TestResult testResult, |
| int numTries) throws UnableToCompleteException { |
| |
| testBatchingMethodTimeoutMillis = batchingStrategy.getTimeoutMultiplier() |
| * baseTestMethodTimeoutMillis; |
| if (mustNotExecuteTest(getBannedPlatforms(testCase.getClass(), |
| testCase.getName()))) { |
| return; |
| } |
| |
| if (lastLaunchFailed) { |
| throw new UnableToCompleteException(); |
| } |
| |
| String moduleName = testCase.getModuleName(); |
| String syntheticModuleName = testCase.getSyntheticModuleName(); |
| Strategy strategy = testCase.getStrategy(); |
| boolean sameTest = (currentModule != null) |
| && syntheticModuleName.equals(currentModule.getName()); |
| if (sameTest && lastLaunchFailed) { |
| throw new UnableToCompleteException(); |
| } |
| |
| // Get the module definition for the current test. |
| if (!sameTest) { |
| currentModule = compileStrategy.maybeCompileModule(moduleName, |
| syntheticModuleName, strategy, batchingStrategy, getTopLogger()); |
| currentCompilationState = currentModule.getCompilationState(getTopLogger()); |
| } |
| assert (currentModule != null); |
| |
| JUnitFatalLaunchException launchException = checkTestClassInCurrentModule( |
| currentCompilationState, moduleName, testCase); |
| if (launchException != null) { |
| testResult.addError(testCase, launchException); |
| return; |
| } |
| |
| currentTestInfo = new TestInfo(currentModule.getName(), |
| testCase.getClass().getName(), testCase.getName()); |
| numTries++; |
| if (messageQueue.hasResults(currentTestInfo)) { |
| // Already have a result. |
| processTestResult(testCase, testResult, strategy); |
| return; |
| } |
| compileStrategy.maybeAddTestBlockForCurrentTest(testCase, batchingStrategy); |
| |
| try { |
| if (firstLaunch) { |
| runStyle.launchModule(currentModule.getName()); |
| } |
| } catch (UnableToCompleteException e) { |
| lastLaunchFailed = true; |
| testResult.addError(testCase, new JUnitFatalLaunchException(e)); |
| return; |
| } |
| |
| boolean mustRetry = mustRetry(numTries); |
| // 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?) |
| testBeginTime = System.currentTimeMillis(); |
| testBeginTimeout = testBeginTime + baseTestBeginTimeoutMillis; |
| testMethodTimeout = 0; // wait until test execution begins |
| while (notDone()) { |
| messageQueue.waitForResults(1000); |
| } |
| if (!mustRetry && pendingException != null) { |
| UnableToCompleteException e = pendingException; |
| pendingException = null; |
| throw e; |
| } |
| } catch (TimeoutException e) { |
| if (!mustRetry) { |
| lastLaunchFailed = true; |
| testResult.addError(testCase, e); |
| return; |
| } |
| } |
| |
| if (mustRetry) { |
| if (messageQueue.needsRerunning(currentTestInfo)) { |
| // remove the result if it is present and rerun |
| messageQueue.removeResults(currentTestInfo); |
| getTopLogger().log(TreeLogger.WARN, |
| currentTestInfo + " is being retried, retry attempt = " + numTries); |
| runTestImpl(testCase, testResult, numTries); |
| return; |
| } |
| } |
| assert (messageQueue.hasResults(currentTestInfo)); |
| processTestResult(testCase, testResult, testCase.getStrategy()); |
| } |
| |
| /** |
| * 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); |
| Pattern quotedArgsPattern = Pattern.compile("^([\"'])(.*)([\"'])$"); |
| |
| while (matcher.find()) { |
| // Strip leading and trailing quotes from the arg |
| String arg = matcher.group(); |
| Matcher qmatcher = quotedArgsPattern.matcher(arg); |
| if (qmatcher.matches()) { |
| argList.add(qmatcher.group(2)); |
| } else { |
| argList.add(arg); |
| } |
| } |
| } |
| |
| return argList.toArray(new String[argList.size()]); |
| } |
| } |