All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.junit.JUnitShell Maven / Gradle / Ivy

/*
 * 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.Linker;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.core.shared.SerializableThrowable;
import com.google.gwt.dev.ArgProcessorBase;
import com.google.gwt.dev.Compiler;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.CompilerOptions;
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.CompilationProblemReporter;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
import com.google.gwt.dev.jjs.JsOutputOption;
import com.google.gwt.dev.shell.jetty.JettyLauncher;
import com.google.gwt.dev.util.arg.ArgHandlerClosureFormattedOutput;
import com.google.gwt.dev.util.arg.ArgHandlerDeployDir;
import com.google.gwt.dev.util.arg.ArgHandlerDeprecatedDisableUpdateCheck;
import com.google.gwt.dev.util.arg.ArgHandlerDeprecatedOptimizeDataflow;
import com.google.gwt.dev.util.arg.ArgHandlerDisableCastChecking;
import com.google.gwt.dev.util.arg.ArgHandlerDisableClassMetadata;
import com.google.gwt.dev.util.arg.ArgHandlerDisableClusterSimilarFunctions;
import com.google.gwt.dev.util.arg.ArgHandlerDisableInlineLiteralParameters;
import com.google.gwt.dev.util.arg.ArgHandlerDisableOrdinalizeEnums;
import com.google.gwt.dev.util.arg.ArgHandlerDisableRemoveDuplicateFunctions;
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.ArgHandlerFilterJsInteropExports;
import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
import com.google.gwt.dev.util.arg.ArgHandlerGenerateJsInteropExports;
import com.google.gwt.dev.util.arg.ArgHandlerIncrementalCompile;
import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
import com.google.gwt.dev.util.arg.ArgHandlerNamespace;
import com.google.gwt.dev.util.arg.ArgHandlerOptimize;
import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle;
import com.google.gwt.dev.util.arg.ArgHandlerSetProperties;
import com.google.gwt.dev.util.arg.ArgHandlerSourceLevel;
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.JUnitHost.TestInfo;
import com.google.gwt.junit.client.impl.JUnitResult;
import com.google.gwt.thirdparty.guava.common.base.Splitter;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
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.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.webapp.WebAppContext;

import java.io.File;
import java.io.IOException;
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.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

/**
 * This class is responsible for hosting JUnit test case execution. There are
 * three main pieces to the JUnit system.
 *
 * 
    *
  • Test environment
  • *
  • Client classes
  • *
  • Server classes
  • *
* *

* 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. *

* *

* 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 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. *

*/ public class JUnitShell extends DevMode { /** * A strategy for running the test. */ public interface Strategy { String getSyntheticModuleExtension(); void processModule(ModuleDef module); } private static class ArgHandlerRunCompiledJavascript extends ArgHandlerFlag { private JUnitShell shell; public ArgHandlerRunCompiledJavascript(JUnitShell shell) { this.shell = shell; addTagValue("-web", false); addTagValue("-prod", false); } @Override public String getPurposeSnippet() { return "Runs tests in Development Mode, using the Java virtual machine."; } @Override public String getLabel() { return "devMode"; } @Override public boolean setFlag(boolean enabled) { shell.developmentMode = enabled; return true; } @Override public boolean getDefaultValue() { return shell.developmentMode; } } private static class ArgHandlerShowWindows extends ArgHandlerFlag { private JUnitShell shell; public ArgHandlerShowWindows(JUnitShell shell) { this.shell = shell; addTagValue("-notHeadless", true); } @Override public String getPurposeSnippet() { return "Causes the log window and browser windows to be displayed; useful for debugging."; } @Override public String getLabel() { return "showUi"; } @Override public boolean setFlag(boolean enabled) { shell.setHeadless(!enabled); return true; } @Override public boolean getDefaultValue() { return !shell.isHeadless(); } } static class ArgProcessor extends ArgProcessorBase { @SuppressWarnings("deprecation") public ArgProcessor(final JUnitShell shell) { final HostedModeOptions options = shell.options; /* * ----- 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 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(shell.new MyJettyLauncher()); // DISABLE: ArgHandlerStartupURLs registerHandler(new ArgHandlerWarDir(options) { private static final String OUT_TAG = "-out"; @Override public String[] getTags() { return new String[] {getTag(), OUT_TAG}; } @Override public int handle(String[] args, int tagIndex) { if (OUT_TAG.equals(args[tagIndex])) { // -out is deprecated. Print a warning message System.err.println("The -out option is deprecated. This option will be removed in " + "future GWT release and will throw an error if it is still used. Please use -war " + "option instead."); } return super.handle(args, tagIndex); } }); registerHandler(new ArgHandlerDeployDir(options)); registerHandler(new ArgHandlerExtraDir(options)); registerHandler(new ArgHandlerWorkDirOptional(options)); registerHandler(new ArgHandlerSourceLevel(options)); // DISABLE: ArgHandlerModuleName /* * ----- Additional options from Compiler not already included ------- */ registerHandler(new ArgHandlerScriptStyle(options)); registerHandler(new ArgHandlerEnableAssertions(options)); registerHandler(new ArgHandlerDisableCastChecking(options)); registerHandler(new ArgHandlerDisableClassMetadata(options)); registerHandler(new ArgHandlerDisableClusterSimilarFunctions(options)); registerHandler(new ArgHandlerDisableInlineLiteralParameters(options)); registerHandler(new ArgHandlerDeprecatedOptimizeDataflow(options)); registerHandler(new ArgHandlerDisableOrdinalizeEnums(options)); registerHandler(new ArgHandlerDisableRemoveDuplicateFunctions(options)); registerHandler(new ArgHandlerDisableRunAsync(options)); registerHandler(new ArgHandlerDeprecatedDisableUpdateCheck()); registerHandler(new ArgHandlerDraftCompile(options)); registerHandler(new ArgHandlerLocalWorkers(options)); registerHandler(new ArgHandlerNamespace(options)); registerHandler(new ArgHandlerOptimize(options)); registerHandler(new ArgHandlerIncrementalCompile(options)); registerHandler(new ArgHandlerGenerateJsInteropExports(options)); registerHandler(new ArgHandlerFilterJsInteropExports(options)); registerHandler(new ArgHandlerSetProperties(options)); registerHandler(new ArgHandlerClosureFormattedOutput(options)); /* * ----- Options specific to JUnitShell ----- */ // Override log level to set WARN by default. registerHandler(new ArgHandlerLogLevel(options, TreeLogger.WARN)); registerHandler(new ArgHandlerRunCompiledJavascript(shell)); 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) { shell.baseTestMethodTimeoutMillis = minutes * 60 * 1000; } }); registerHandler(new ArgHandlerInt() { @Override public String[] getDefaultArgs() { return new String[]{getTag(), String.valueOf(DEFAULT_BEGIN_TIMEOUT_MINUTES)}; } @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) { shell.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) { shell.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")) { shell.batchingStrategy = new NoBatchingStrategy(); } else if (str.equals("class")) { shell.batchingStrategy = new ClassBatchingStrategy(); } else if (str.equals("module")) { shell.batchingStrategy = new ModuleBatchingStrategy(); } else { return false; } return true; } }); registerHandler(new ArgHandlerShowWindows(shell)); 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")) { shell.compileStrategy = new SimpleCompileStrategy(shell); } else if (str.equals("all")) { shell.compileStrategy = new PreCompileStrategy(shell); } else if (str.equals("parallel")) { shell.compileStrategy = new ParallelCompileStrategy(shell); } else { return false; } 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) { shell.tries = value; } @Override public boolean isExperimental() { return true; } }); registerHandler(new ArgHandlerString() { @Override public String getPurpose() { return "Specify the user agents to reduce the number of permutations for remote browser tests;" + " e.g. ie8,safari,gecko1_8"; } @Override public String getTag() { return "-userAgents"; } @Override public String[] getTagArgs() { return new String[]{"userAgents"}; } @Override public boolean setString(String str) { Splitter splitter = Splitter.on(",").omitEmptyStrings().trimResults(); shell.userAgentsOpt = ImmutableSet.copyOf(splitter.split(str)); 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). */ @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.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false"); // Prefer the parent class loader so that JUnit works. setParentLoaderPriority(true); } }; } } /** * How many minutes to wait for the browser to contact the test system. */ private static final int DEFAULT_BEGIN_TIMEOUT_MINUTES = 1; /** * 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 null if no such * singleton exists. */ public static JUnitMessageQueue getMessageQueue() { if (unitTestShell == null) { return null; } return unitTestShell.messageQueue; } /** * Get the set of remote user agents to compile. * * @return the set of remote user agents */ public static Set getRemoteUserAgents() { if (unitTestShell == null) { return null; } return unitTestShell.runStyle.getUserAgents(); } /** * Get the compiler options * * @return the compiler options that have been set. */ public static CompilerOptions getCompilerOptions() { if (unitTestShell == null) { return null; } return unitTestShell.options; } /** * 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); } /** * 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 = new ArgProcessor(unitTestShell); 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", e); } 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(TreeLogger logger, 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 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 { CompilationProblemReporter.logErrorTrace(logger, TreeLogger.ERROR, compilationState.getCompilerContext(), typeName, true); errMsg = "The test class '" + typeName + "' had compile errors; check log for details"; } 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 getBannedPlatforms(Class testClass, String methodName) { Set 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 last 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 = false; /** * Used to make sure we don't start the runStyle more than once. */ private boolean runStyleStarted; /** * If true, we haven't started all the clients yet. (Used for manual mode.) */ private boolean waitingForClients = 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 loadedServletsByPath = new HashMap(); /** * 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 so we can limit permutations for remote tests. */ Set userAgentsOpt; // Visible for testing /** * 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"; /** * 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; /** * Visible for testing only. (See {@link #getUnitTestShell}.) */ JUnitShell() { setRunTomcat(true); setHeadless(true); } public String getModuleUrl(String moduleName) { // TODO(manolo): consider using DevModeBase.normalizeURL // and DevModeBase.makeStartupUrl instead. String localhost = runStyle.getLocalHostName(); int codeServerPort = developmentMode ? listener.getSocketPort() : 0; return getModuleUrl(localhost, getPort(), moduleName, codeServerPort); } public CompilerContext getCompilerContext() { return compilerContext; } @Override protected HostedModeOptions createOptions() { HostedModeOptions options = super.createOptions(); options.setSuperDevMode(false); options.setIncrementalCompileEnabled(false); return options; } @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 (userAgentsOpt != null) { runStyle.setUserAgents(userAgentsOpt); } 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 void inferStartupUrls() { // do nothing -- JUnitShell isn't expected to have startup URLs } @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 (runStyle instanceof RunStyleManual && waitingForClients) { 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; } waitingForClients = false; } long currentTimeMillis = System.currentTimeMillis(); if (activeClients >= expectedClients) { if (activeClients > expectedClients) { getTopLogger().log( TreeLogger.WARN, "Too many clients: expected " + expectedClients + ", found " + activeClients); } /* * 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" + "Try increasing this timeout using the '-testMethodTimeout minutes' option\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" + "Try increasing this timeout using the '-testBeginTimeout minutes' option\n" + "The default value of minutes is 1, i.e., the server waits 1 minute or 60 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, Set userAgents) throws UnableToCompleteException { if (userAgents != null && !userAgents.isEmpty()) { Properties props = module.getProperties(); Property userAgent = props.find("user.agent"); if (userAgent instanceof BindingProperty) { BindingProperty bindingProperty = (BindingProperty) userAgent; bindingProperty.setRootGeneratedValues(userAgents.toArray(new String[0])); } } if (options.isClosureCompilerFormatEnabled()) { module.addLinker("closureHelpers"); } boolean success = false; try { success = Compiler.compile(getTopLogger(), options, module); } catch (Exception e) { getTopLogger().log(Type.ERROR, "Compiler aborted with an exception ", e); } if (!success) { throw new UnableToCompleteException(); } // TODO(scottb): prepopulate currentCompilationState somehow? } String getModuleUrl(String hostName, int port, String moduleName, int codeServerPort) { String url = "http://" + hostName + ":" + port + "/" + moduleName + "/junit.html"; if (developmentMode) { url += "?gwt.codesvr=" + hostName + ":" + codeServerPort; } return url; } void maybeCompileForWebMode(ModuleDef module, Set userAgents) throws UnableToCompleteException { compilerContext = compilerContextBuilder.module(module).build(); // 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 { // We should load the class ourselves because otherwise if Jetty tries and fails to load // by itself, it will be left in a broken state (looks like this is fixed in Jetty 9). Class clazz = wac.loadClass(servletClass).asSubclass(Servlet.class); 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); } } } BindingProperty strictCspTestingEnabledProperty = module.getProperties().findBindingProp("gwt.strictCspTestingEnabled"); if (strictCspTestingEnabledProperty != null && "true".equals(strictCspTestingEnabledProperty.getConstrainedValue())) { addCspFilter("/" + module.getName() + "/*"); } if (developmentMode) { // BACKWARDS COMPATIBILITY: many linkers currently fail in dev mode. try { Linker l = module.getActivePrimaryLinker().newInstance(); StandardLinkerContext context = new StandardLinkerContext( getTopLogger(), module, compilerContext.getPublicResourceOracle(), JsOutputOption.PRETTY); if (!l.supportsDevModeInJunit(context)) { if (module.getLinker("std") != null) { // TODO: unfortunately, this could be race condition between dev/prod module.addLinker("std"); } } } catch (Exception e) { getTopLogger().log(TreeLogger.WARN, "Failed to instantiate linker: " + e); } super.link(getTopLogger(), module); } else { compileForWebMode(module, userAgents); } } /** * Adds a filter to the server that automatically adds Content-Security-Policy HTTP headers to * responses on the given path. */ private void addCspFilter(String path) { wac.addFilter(new FilterHolder(new Filter() { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.addHeader("Content-Security-Policy", "script-src 'nonce-gwt-nonce' 'unsafe-inline' 'strict-dynamic' https: http: " + "'unsafe-eval' 'report-sample'"); chain.doFilter(request, response); } @Override public void init(FilterConfig arg0) throws ServletException { } @Override public void destroy() { } }), path, EnumSet.of(DispatcherType.REQUEST)); } 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 runStyleClass = clazz.asSubclass(RunStyle.class); Constructor 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 bannedPlatforms) { if (!Collections.disjoint(bannedPlatforms, runStyle.getPlatforms())) { 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) { Map results = messageQueue.getResults(currentTestInfo); assert results != null; assert results.size() == messageQueue.getNumClients() : results.size() + " != " + messageQueue.getNumClients(); for (Entry entry : results.entrySet()) { JUnitResult result = entry.getValue(); assert (result != null); if (result.isAnyException()) { if (result.isExceptionOf(AssertionFailedError.class)) { testResult.addFailure(testCase, toAssertionFailedError(result.getException())); } else { testResult.addError(testCase, result.getException()); } } } } private AssertionFailedError toAssertionFailedError(SerializableThrowable thrown) { AssertionFailedError error = new AssertionFailedError(thrown.getMessage()); error.setStackTrace(thrown.getStackTrace()); if (thrown.getCause() != null) { error.initCause(thrown.getCause()); } return error; } 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) { try { currentModule = compileStrategy.maybeCompileModule(moduleName, syntheticModuleName, strategy, batchingStrategy, getTopLogger()); compilerContext = compilerContextBuilder.module(currentModule).build(); currentCompilationState = currentModule.getCompilationState(getTopLogger(), compilerContext); } catch (UnableToCompleteException e) { lastLaunchFailed = true; throw e; } } assert (currentModule != null); JUnitFatalLaunchException launchException = checkTestClassInCurrentModule(getTopLogger(), 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); return; } compileStrategy.maybeAddTestBlockForCurrentTest(testCase, batchingStrategy); try { if (!runStyleStarted) { runStyle.launchModule(currentModule.getName()); } } catch (UnableToCompleteException e) { lastLaunchFailed = true; testResult.addError(testCase, new JUnitFatalLaunchException(e)); return; } runStyleStarted = true; 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); } /** * Synthesize command line arguments from a system property. */ private String[] synthesizeArgs() { ArrayList argList = new ArrayList(); 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()]); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy