Re-rolling r7955: JUnitShell now subclasses DevMode instead of GWTShell.

http://gwt-code-reviews.appspot.com/361801/show
Review by: jlabanca,jat,spoon,fabbott,amitmanjhi


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7971 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/Compiler.java b/dev/core/src/com/google/gwt/dev/Compiler.java
index 7024343..31d8286 100644
--- a/dev/core/src/com/google/gwt/dev/Compiler.java
+++ b/dev/core/src/com/google/gwt/dev/Compiler.java
@@ -165,6 +165,16 @@
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
+    ModuleDef[] modules = new ModuleDef[options.getModuleNames().size()];
+    int i = 0;
+    for (String moduleName : options.getModuleNames()) {
+      modules[i++] = ModuleDefLoader.loadFromClassPath(logger, moduleName, true);
+    }
+    return run(logger, modules);
+  }
+
+  public boolean run(TreeLogger logger, ModuleDef... modules)
+      throws UnableToCompleteException {
     PerfLogger.start("compile");
     boolean tempWorkDir = false;
     try {
@@ -176,10 +186,8 @@
         options.setExtraDir(new File("extras"));
       }
 
-      for (String moduleName : options.getModuleNames()) {
-        ModuleDef module = ModuleDefLoader.loadFromClassPath(logger,
-            moduleName, true);
-
+      for (ModuleDef module : modules) {
+        String moduleName = module.getCanonicalName();
         if (options.isValidateOnly()) {
           if (!Precompile.validate(logger, options, module,
               options.getGenDir(), options.getDumpSignatureFile())) {
diff --git a/dev/core/src/com/google/gwt/dev/DevMode.java b/dev/core/src/com/google/gwt/dev/DevMode.java
index 4b1f0b0..5dda46e 100644
--- a/dev/core/src/com/google/gwt/dev/DevMode.java
+++ b/dev/core/src/com/google/gwt/dev/DevMode.java
@@ -155,7 +155,10 @@
     }
   }
 
-  static class ArgProcessor extends DevModeBase.ArgProcessor {
+  /**
+   * The argument processor.
+   */
+  protected static class ArgProcessor extends DevModeBase.ArgProcessor {
     public ArgProcessor(HostedModeOptions options) {
       super(options, false);
       registerHandler(new ArgHandlerServer(options));
@@ -177,7 +180,11 @@
     }
   }
 
-  interface HostedModeOptions extends HostedModeBaseOptions, CompilerOptions {
+  /**
+   * Options controlling dev mode.
+   */
+  protected interface HostedModeOptions extends HostedModeBaseOptions,
+      CompilerOptions {
     ServletContainerLauncher getServletContainerLauncher();
 
     String getServletContainerLauncherArgs();
@@ -190,11 +197,10 @@
   /**
    * Concrete class to implement all hosted mode options.
    */
-  static class HostedModeOptionsImpl extends HostedModeBaseOptionsImpl
-      implements HostedModeOptions {
+  protected static class HostedModeOptionsImpl extends
+      HostedModeBaseOptionsImpl implements HostedModeOptions {
     private File extraDir;
     private int localWorkers;
-    private File outDir;
     private ServletContainerLauncher scl;
     private String sclArgs;
     private File warDir;
@@ -209,7 +215,7 @@
 
     @Deprecated
     public File getOutDir() {
-      return outDir;
+      return warDir;
     }
 
     public ServletContainerLauncher getServletContainerLauncher() {
@@ -234,7 +240,7 @@
 
     @Deprecated
     public void setOutDir(File outDir) {
-      this.outDir = outDir;
+      this.warDir = outDir;
     }
 
     public void setServletContainerLauncher(ServletContainerLauncher scl) {
@@ -300,7 +306,7 @@
   /**
    * Default constructor for testing; no public API yet.
    */
-  DevMode() {
+  protected DevMode() {
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/DevModeBase.java b/dev/core/src/com/google/gwt/dev/DevModeBase.java
index b7a9eb7..2d1f2a7 100644
--- a/dev/core/src/com/google/gwt/dev/DevModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/DevModeBase.java
@@ -62,7 +62,7 @@
  * The main executable class for the hosted mode shell. This class must not have
  * any GUI dependencies.
  */
-abstract class DevModeBase implements DoneCallback {
+public abstract class DevModeBase implements DoneCallback {
 
   /**
    * Implementation of BrowserWidgetHost that supports the abstract UI
@@ -165,6 +165,9 @@
    * Handles the -blacklist command line argument.
    */
   protected static class ArgHandlerBlacklist extends ArgHandlerString {
+    public ArgHandlerBlacklist() {
+    }
+
     @Override
     public String getPurpose() {
       return "Prevents the user browsing URLs that match the specified regexes (comma or space separated)";
@@ -343,6 +346,9 @@
     }
   }
 
+  /**
+   * Handles the -remoteUI command line flag.
+   */
   protected static class ArgHandlerRemoteUI extends ArgHandlerString {
 
     private final HostedModeBaseOptions options;
@@ -407,6 +413,9 @@
    * Handles the -whitelist command line flag.
    */
   protected static class ArgHandlerWhitelist extends ArgHandlerString {
+    public ArgHandlerWhitelist() {
+    }
+
     @Override
     public String getPurpose() {
       return "Allows the user to browse URLs that match the specified regexes (comma or space separated)";
@@ -428,6 +437,9 @@
     }
   }
 
+  /**
+   * Base options for dev mode.
+   */
   protected interface HostedModeBaseOptions extends JJSOptions, OptionLogDir,
       OptionLogLevel, OptionGenDir, OptionNoServer, OptionPort,
       OptionCodeServerPort, OptionStartupURLs, OptionRemoteUI,
@@ -548,6 +560,9 @@
     }
   }
 
+  /**
+   * Controls what local address to bind to.
+   */
   protected interface OptionBindAddress {
     String getBindAddress();
 
@@ -558,6 +573,9 @@
     void setConnectAddress(String connectAddress);
   }
 
+  /**
+   * Controls what port the code server listens on.
+   */
   protected interface OptionCodeServerPort {
     int getCodeServerPort();
 
@@ -626,7 +644,10 @@
     List<String> getStartupURLs();
   }
 
-  abstract static class ArgProcessor extends ArgProcessorBase {
+  /**
+   * The base dev mode argument processor.
+   */
+  protected abstract static class ArgProcessor extends ArgProcessorBase {
     public ArgProcessor(HostedModeBaseOptions options, boolean forceServer) {
       if (!forceServer) {
         registerHandler(new ArgHandlerNoServerFlag(options));
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
index 4663d23..3dedb92 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -533,8 +533,7 @@
     server.addConnector(connector);
 
     // Create a new web app in the war directory.
-    WebAppContext wac = new WebAppContextWithReload(logger,
-        appRootDir.getAbsolutePath(), "/");
+    WebAppContext wac = createWebAppContext(logger, appRootDir);
 
     RequestLogHandler logHandler = new RequestLogHandler();
     logHandler.setRequestLog(new JettyRequestLogger(logger, getBaseLogLevel()));
@@ -546,8 +545,18 @@
     // Now that we're started, log to the top level logger.
     Log.setLog(new JettyTreeLogger(logger));
 
-    return new JettyServletContainer(logger, server, wac,
-        connector.getLocalPort(), appRootDir);
+    return createServletContainer(logger, appRootDir, server, wac,
+        connector.getLocalPort());
+  }
+
+  protected JettyServletContainer createServletContainer(TreeLogger logger,
+      File appRootDir, Server server, WebAppContext wac, int localPort) {
+    return new JettyServletContainer(logger, server, wac, localPort, appRootDir);
+  }
+
+  protected WebAppContext createWebAppContext(TreeLogger logger, File appRootDir) {
+    return new WebAppContextWithReload(logger, appRootDir.getAbsolutePath(),
+        "/");
   }
 
   protected AbstractConnector getConnector() {
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerWarDir.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerWarDir.java
index 5077850..1f14f90 100644
--- a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerWarDir.java
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerWarDir.java
@@ -22,7 +22,7 @@
 /**
  * Argument handler for processing the output directory flag.
  */
-public final class ArgHandlerWarDir extends ArgHandlerDir {
+public class ArgHandlerWarDir extends ArgHandlerDir {
 
   private final OptionWarDir option;
 
diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath
index 9f5fd37..0765257 100644
--- a/eclipse/user/.classpath
+++ b/eclipse/user/.classpath
@@ -31,5 +31,6 @@
 	<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/htmlunit/htmlunit-r5607/htmlunit-r5607.jar" sourcepath="/GWT_TOOLS/lib/htmlunit/htmlunit-r5607/htmlunit-r5607-sources.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/htmlunit/htmlunit-r5607/htmlunit-core-js-r5607.jar" sourcepath="/GWT_TOOLS/lib/htmlunit/htmlunit-r5607/htmlunit-core-js-r5607-sources.jar"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/user/build.xml b/user/build.xml
index 8af9a42..cac355b 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -36,13 +36,6 @@
   <property name="test.args.web.selenium" value="${test.args}" />
 
   <!--
-    Only IFrameLinker actually supports -noserver mode; run the other linker
-    tests if and when they are supported
-  -->
-  <property name="gwt.junit.testcase.noserver.includes" value="**/IFrameLinkerTest.class" />
-  <property name="gwt.junit.testcase.noserver.excludes" value="" />
-
-  <!--
     Whether I18NSuite should test e.g. Foo$InnerMsgs_fr.properties (if the
     value is "dollar") or Foo_Inner_fr.properties (for "bar")
    -->
@@ -394,22 +387,6 @@
     <echo message="DEPRECATED: test.hosted has been renamed test.dev"/>
   </target>
 
-  <target name="test.noserver"
-      depends="compile, compile.tests"
-      description="Run noserver tests for this project."
-      unless="test.noserver.disable">
-    <fileset id="test.noserver.tests" dir="${javac.junit.out}"
-        includes="${gwt.junit.testcase.noserver.includes}"
-        excludes="${gwt.junit.testcase.noserver.excludes}" />
-    <gwt.junit test.name="test.noserver"
-        test.args="${test.args} -prod -standardsMode -noserver"
-        test.out="${junit.out}/noserver" test.cases="test.noserver.tests">
-      <extraclasspaths>
-        <path refid="test.extraclasspath" />
-      </extraclasspaths>
-    </gwt.junit>
-  </target>
-
   <target name="test.web.htmlunit"
       depends="compile, compile.tests"
       description="Run web-mode tests with HtmlUnit."
@@ -489,7 +466,6 @@
       <antcall target="test.web.selenium"/>
       <antcall target="test.draft.selenium"/>
       <antcall target="test.nometa.selenium"/>
-      <antcall target="test.noserver"/>
       <antcall target="test.dev.htmlunit"/>
       <antcall target="test.emma.htmlunit"/>
       <antcall target="test.web.htmlunit"/>
@@ -516,7 +492,6 @@
       <antcall target="test.web.htmlunit"/>
       <antcall target="test.draft.htmlunit"/>
       <antcall target="test.nometa.htmlunit"/>
-      <antcall target="test.noserver"/>
     </parallel>
     </limit>
   </target>
diff --git a/user/src/com/google/gwt/junit/CompileStrategy.java b/user/src/com/google/gwt/junit/CompileStrategy.java
index b6917b7..cc62ecd 100644
--- a/user/src/com/google/gwt/junit/CompileStrategy.java
+++ b/user/src/com/google/gwt/junit/CompileStrategy.java
@@ -181,7 +181,7 @@
 
     strategy.processModule(moduleDef);
 
-    junitShell.maybeCompileForWebMode(syntheticModuleName,
+    junitShell.maybeCompileForWebMode(moduleDef,
         JUnitShell.getRemoteUserAgents());
 
     return moduleDef;
diff --git a/user/src/com/google/gwt/junit/JUnit.gwt.xml b/user/src/com/google/gwt/junit/JUnit.gwt.xml
index 8963a34..c2ae43c 100644
--- a/user/src/com/google/gwt/junit/JUnit.gwt.xml
+++ b/user/src/com/google/gwt/junit/JUnit.gwt.xml
@@ -38,7 +38,7 @@
   <!-- Override the regular symbolMaps linker to put the data somewhere we can find it -->
   <define-linker name="symbolMaps" class="com.google.gwt.junit.linker.JUnitSymbolMapsLinker" />
 
-  <servlet path='/junithost' class='com.google.gwt.junit.server.JUnitHostImpl'/>
+  <servlet path='/junithost/*' class='com.google.gwt.junit.server.JUnitHostImpl'/>
 
   <inherits name="com.google.gwt.benchmarks.Benchmarks"/>
 
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 4c0e927..2c2ff0f 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -20,26 +20,31 @@
 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.GWTCompiler;
-import com.google.gwt.dev.GWTShell;
-import com.google.gwt.dev.LegacyCompilerOptions;
-import com.google.gwt.dev.GWTCompiler.GWTCompilerOptionsImpl;
+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.ModuleDefLoader;
 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;
@@ -53,6 +58,10 @@
 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;
@@ -61,6 +70,7 @@
 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;
@@ -96,8 +106,7 @@
  * {@link JUnitMessageQueue}, thus closing the loop.
  * </p>
  */
-@SuppressWarnings("deprecation")
-public class JUnitShell extends GWTShell {
+public class JUnitShell extends DevMode {
 
   /**
    * A strategy for running the test.
@@ -112,54 +121,73 @@
     void processResult(TestCase testCase, JUnitResult result);
   }
 
-  class ArgProcessor extends GWTShell.ArgProcessor {
+  class ArgProcessor extends ArgProcessorBase {
 
+    @SuppressWarnings("deprecation")
     public ArgProcessor() {
-      super(options, true, true);
+      /*
+       * ----- 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));
 
-      // Override port to set auto by default.
-      registerHandler(new ArgHandlerPort(options) {
-        @Override
-        public String[] getDefaultArgs() {
-          return new String[]{"-port", "auto"};
-        }
-      });
-
-      // Override port to set auto by default.
-      registerHandler(new ArgHandlerCodeServerPort(options) {
-        @Override
-        public String[] getDefaultArgs() {
-          return new String[]{this.getTag(), "auto"};
-        }
-      });
-
-      // Disable -bindAddress, fail if it is given
-      // TODO(jat): support -bindAddress in JUnitShell, which will probably
-      // require changes to the RunStyle API.
-      registerHandler(new ArgHandlerBindAddress(options) {
-        @Override
-        public String[] getDefaultArgs() {
-          return null;
-        }
-
-        @Override
-        public boolean isUndocumented() {
-          return true;
-        }
-
-        @Override
-        public boolean setString(String value) {
-          System.err.println("-bindAddress is not supported for JUnitShell");
-          return false;
-        }
-      });
+      /*
+       * ----- Options specific to JUnitShell -----
+       */
 
       // Override log level to set WARN by default..
       registerHandler(new ArgHandlerLogLevel(options) {
@@ -172,24 +200,6 @@
       registerHandler(new ArgHandlerFlag() {
         @Override
         public String getPurpose() {
-          return "Causes your test to run in -noserver development mode (defaults to development mode)";
-        }
-
-        @Override
-        public String getTag() {
-          return "-noserver";
-        }
-
-        @Override
-        public boolean setFlag() {
-          shouldAutoGenerateResources = false;
-          return true;
-        }
-      });
-
-      registerHandler(new ArgHandlerFlag() {
-        @Override
-        public String getPurpose() {
           return "Synonym for -prod (deprecated)";
         }
 
@@ -374,7 +384,7 @@
 
         @Override
         public boolean setFlag() {
-          setHeadlessAccessor(false);
+          JUnitShell.this.setHeadless(false);
           return true;
         }
       });
@@ -492,6 +502,49 @@
         }
       });
     }
+
+    @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);
+        }
+      };
+    }
   }
 
   /**
@@ -597,6 +650,39 @@
   }
 
   /**
+   * 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.
@@ -660,37 +746,9 @@
   }
 
   /**
-   * Retrieves the JUnitShell. This should only be invoked during TestRunner
-   * execution of JUnit tests.
+   * Our server's web app context; used to dynamically add servlets.
    */
-  private 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;
-  }
+  WebAppContext wac;
 
   /**
    * The amount of time to wait for all clients to have contacted the server and
@@ -758,6 +816,11 @@
   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;
@@ -785,8 +848,6 @@
    */
   private String runStyleName = "HtmlUnit";
 
-  private boolean shouldAutoGenerateResources = true;
-
   private boolean standardsMode = false;
 
   /**
@@ -987,20 +1048,12 @@
   }
 
   @Override
-  protected boolean shouldAutoGenerateResources() {
-    return shouldAutoGenerateResources;
-  }
-
-  @Override
   protected void warnAboutNoStartupUrls() {
     // do nothing -- JUnitShell isn't expected to have startup URLs
   }
 
-  void compileForWebMode(String moduleName, String... userAgents)
+  void compileForWebMode(ModuleDef module, String... userAgents)
       throws UnableToCompleteException {
-    // Never fresh during JUnit.
-    ModuleDef module = ModuleDefLoader.loadFromClassPath(getTopLogger(),
-        moduleName, false);
     if (userAgents != null && userAgents.length > 0) {
       Properties props = module.getProperties();
       Property userAgent = props.find("user.agent");
@@ -1010,28 +1063,41 @@
             userAgents);
       }
     }
-    LegacyCompilerOptions newOptions = new GWTCompilerOptionsImpl(options);
-    if (!new GWTCompiler(newOptions).run(getTopLogger(), module)) {
+    if (!new Compiler(options).run(getTopLogger(), module)) {
       throw new UnableToCompleteException();
     }
     // TODO(scottb): prepopulate currentCompilationState somehow?
   }
 
-  void maybeCompileForWebMode(String moduleName, String... userAgents)
+  void maybeCompileForWebMode(ModuleDef module, String... userAgents)
       throws UnableToCompleteException {
-    if (!developmentMode || !shouldAutoGenerateResources) {
-      compileForWebMode(moduleName, userAgents);
+    // 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);
+        }
+      }
     }
-  }
-
-  /**
-   * Accessor method to DevModeBase.setHeadless -- without this, we get
-   * IllegalAccessError from the -notHeadless arg handler. Compiler bug?
-   * 
-   * @param headlessMode
-   */
-  void setHeadlessAccessor(boolean headlessMode) {
-    setHeadless(headlessMode);
+    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);
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 958d05a..1915918 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -111,10 +111,11 @@
   public InitialResponse getTestBlock(int blockIndex, ClientInfo clientInfo)
       throws TimeoutException {
     ClientInfoExt clientInfoExt;
+    HttpServletRequest request = getThreadLocalRequest();
     if (clientInfo.getSessionId() < 0) {
-      clientInfoExt = createNewClientInfo(clientInfo.getUserAgent());
+      clientInfoExt = createNewClientInfo(clientInfo.getUserAgent(), request);
     } else {
-      clientInfoExt = createClientInfo(clientInfo);
+      clientInfoExt = createClientInfo(clientInfo, request);
     }
     TestBlock initialTestBlock = getHost().getTestBlock(clientInfoExt,
         blockIndex, TIME_TO_WAIT_FOR_TESTNAME);
@@ -131,7 +132,8 @@
       result.setException(deserialize(ew));
     }
     JUnitMessageQueue host = getHost();
-    ClientInfoExt clientInfoExt = createClientInfo(clientInfo);
+    ClientInfoExt clientInfoExt = createClientInfo(clientInfo,
+        getThreadLocalRequest());
     host.reportResults(clientInfoExt, results);
     return host.getTestBlock(clientInfoExt, testBlock,
         TIME_TO_WAIT_FOR_TESTNAME);
@@ -146,21 +148,23 @@
       JUnitResult result = new JUnitResult();
       initResult(request, result);
       result.setException(new JUnitFatalLaunchException(requestPayload));
-      getHost().reportFatalLaunch(createNewClientInfo(null), result);
+      getHost().reportFatalLaunch(createNewClientInfo(null, request), result);
     } else {
       super.service(request, response);
     }
   }
 
-  private ClientInfoExt createClientInfo(ClientInfo clientInfo) {
+  private ClientInfoExt createClientInfo(ClientInfo clientInfo,
+      HttpServletRequest request) {
     assert (clientInfo.getSessionId() >= 0);
     return new ClientInfoExt(clientInfo.getSessionId(),
-        clientInfo.getUserAgent(), getClientDesc(getThreadLocalRequest()));
+        clientInfo.getUserAgent(), getClientDesc(request));
   }
 
-  private ClientInfoExt createNewClientInfo(String userAgent) {
+  private ClientInfoExt createNewClientInfo(String userAgent,
+      HttpServletRequest request) {
     return new ClientInfoExt(createSessionId(), userAgent,
-        getClientDesc(getThreadLocalRequest()));
+        getClientDesc(request));
   }
 
   private int createSessionId() {
@@ -346,12 +350,12 @@
 
       JsniRef ref = JsniRef.parse(parts[0].substring(0,
           parts[0].lastIndexOf(')') + 1));
-      toReturn = new Object[] {
+      toReturn = new Object[]{
           ref.className(), ref.memberName(), stw.fileName, stw.lineNumber};
 
     } else {
       // Use the raw data from the client
-      toReturn = new Object[] {
+      toReturn = new Object[]{
           stw.className, stw.methodName, stw.fileName, stw.lineNumber};
     }
     return toReturn;
diff --git a/user/test-super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java b/user/test-super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java
new file mode 100644
index 0000000..ffdda16
--- /dev/null
+++ b/user/test-super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010 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.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+import junit.framework.TestCase;
+
+/**
+ * Translatable trivial implementation.
+ */
+@DoNotRunWith(Platform.Prod)
+public class DevModeOnCompiledScriptTest extends GWTTestCase {
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.DevModeWithCompiledScriptTest";
+  }
+
+  public void testSomethingTrivial() {
+    assertTrue(true);
+  }
+}
diff --git a/user/test/com/google/gwt/core/ext/test/XSLinkerTest.java b/user/test/com/google/gwt/core/ext/test/XSLinkerTest.java
index 9b9d019..64da6cd 100644
--- a/user/test/com/google/gwt/core/ext/test/XSLinkerTest.java
+++ b/user/test/com/google/gwt/core/ext/test/XSLinkerTest.java
@@ -15,9 +15,13 @@
  */
 package com.google.gwt.core.ext.test;
 
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
 /**
  * Tests the cross-site linker.
  */
+@DoNotRunWith(Platform.Devel)
 public class XSLinkerTest extends LinkerTest {
   @Override
   public String getModuleName() {
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
index 9e5ae07..0fa0e34 100644
--- a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
@@ -15,7 +15,7 @@
 <module>
   <inherits name="com.google.gwt.core.Core" />
   <source path="test" />
-  <servlet path="/runAsyncFailure"
+  <servlet path="/runAsyncFailure/*"
     class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet" />
   <set-configuration-property name="iframe.linker.deferredjs.subdir"
     value="runAsyncFailure/deferredjs" />
diff --git a/user/test/com/google/gwt/http/RequestBuilderTest.gwt.xml b/user/test/com/google/gwt/http/RequestBuilderTest.gwt.xml
index 0a5e76f..9e01f79 100644
--- a/user/test/com/google/gwt/http/RequestBuilderTest.gwt.xml
+++ b/user/test/com/google/gwt/http/RequestBuilderTest.gwt.xml
@@ -15,6 +15,6 @@
 <module>
   <inherits name='com.google.gwt.user.User' />
 
-  <servlet path='/testRequestBuilder'
+  <servlet path='/testRequestBuilder/*'
     class='com.google.gwt.http.server.RequestBuilderTestServlet' />
 </module>
diff --git a/user/test/com/google/gwt/http/ResponseTest.gwt.xml b/user/test/com/google/gwt/http/ResponseTest.gwt.xml
index e3c0479..55735ff 100644
--- a/user/test/com/google/gwt/http/ResponseTest.gwt.xml
+++ b/user/test/com/google/gwt/http/ResponseTest.gwt.xml
@@ -15,6 +15,6 @@
 <module>
   <inherits name='com.google.gwt.user.User' />
 
-  <servlet path='/testResponse'
+  <servlet path='/testResponse/*'
     class='com.google.gwt.http.server.ResponseTestServlet' />
 </module>
diff --git a/user/test/com/google/gwt/http/server/RequestBuilderTestServlet.java b/user/test/com/google/gwt/http/server/RequestBuilderTestServlet.java
index e07692b..abfef22 100644
--- a/user/test/com/google/gwt/http/server/RequestBuilderTestServlet.java
+++ b/user/test/com/google/gwt/http/server/RequestBuilderTestServlet.java
@@ -30,10 +30,6 @@
  */
 public class RequestBuilderTestServlet extends HttpServlet {
 
-  private static String getPathInfoBase() {
-    return "/com.google.gwt.http.RequestBuilderTest.JUnit/testRequestBuilder/";
-  }
-
   @Override
   protected void doDelete(HttpServletRequest request,
       HttpServletResponse response) {
@@ -49,7 +45,7 @@
   protected void doGet(HttpServletRequest request, HttpServletResponse response)
       throws IOException {
     String pathInfo = request.getPathInfo();
-    if (pathInfo.equals(getPathInfoBase() + "setRequestHeader")) {
+    if (pathInfo.equals("/setRequestHeader")) {
       String value = request.getHeader("Foo");
       if (value.equals("Bar1")) {
         response.setStatus(HttpServletResponse.SC_OK);
@@ -57,13 +53,13 @@
       } else {
         response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       }
-    } else if (pathInfo.equals(getPathInfoBase() + "send_GET")) {
+    } else if (pathInfo.equals("/send_GET")) {
       response.setStatus(HttpServletResponse.SC_OK);
       response.getWriter().write(RequestBuilderTest.SERVLET_GET_RESPONSE);
-    } else if (pathInfo.equals(getPathInfoBase() + "sendRequest_GET")) {
+    } else if (pathInfo.equals("/sendRequest_GET")) {
       response.setStatus(HttpServletResponse.SC_OK);
       response.getWriter().write(RequestBuilderTest.SERVLET_GET_RESPONSE);
-    } else if (pathInfo.equals(getPathInfoBase() + "setTimeout/timeout")) {
+    } else if (pathInfo.equals("/setTimeout/timeout")) {
       // cause a timeout on the client
       try {
         Thread.sleep(5000);
@@ -71,7 +67,7 @@
       }
       response.setStatus(HttpServletResponse.SC_OK);
       response.getWriter().print(RequestBuilderTest.SERVLET_GET_RESPONSE);
-    } else if (pathInfo.equals(getPathInfoBase() + "setTimeout/noTimeout")) {
+    } else if (pathInfo.equals("/setTimeout/noTimeout")) {
       // wait but not long enough to timeout
       try {
         Thread.sleep(1000);
@@ -79,7 +75,7 @@
       }
       response.setStatus(HttpServletResponse.SC_OK);
       response.getWriter().print(RequestBuilderTest.SERVLET_GET_RESPONSE);
-    } else if (pathInfo.equals(getPathInfoBase() + "user/pass")) {
+    } else if (pathInfo.equals("/user/pass")) {
       String auth = request.getHeader("Authorization");
       if (auth == null) {
         response.setHeader("WWW-Authenticate", "BASIC");
@@ -111,7 +107,7 @@
          */
         response.getWriter().print(RequestBuilderTest.SERVLET_POST_RESPONSE);
         response.setStatus(HttpServletResponse.SC_OK);
-      } else if (request.getPathInfo().equals(getPathInfoBase() + "simplePost")) {
+      } else if (request.getPathInfo().equals("/simplePost")) {
         response.getWriter().print(RequestBuilderTest.SERVLET_POST_RESPONSE);
         response.setStatus(HttpServletResponse.SC_OK);
       } else {
diff --git a/user/test/com/google/gwt/junit/DevModeOnCompiledScriptTest.gwt.xml b/user/test/com/google/gwt/junit/DevModeOnCompiledScriptTest.gwt.xml
new file mode 100644
index 0000000..a4c0532
--- /dev/null
+++ b/user/test/com/google/gwt/junit/DevModeOnCompiledScriptTest.gwt.xml
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- Copyright 2010 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name='com.google.gwt.junit.JUnitTest' />
+</module>
diff --git a/user/test/com/google/gwt/junit/JUnitBridge.java b/user/test/com/google/gwt/junit/JUnitBridge.java
new file mode 100644
index 0000000..cb69546
--- /dev/null
+++ b/user/test/com/google/gwt/junit/JUnitBridge.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010 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.UnableToCompleteException;
+import com.google.gwt.dev.cfg.ModuleDef;
+
+/**
+ * Provides access to {@link JUnitShell}.
+ */
+public class JUnitBridge {
+  public static void compileForWebMode(ModuleDef module)
+      throws UnableToCompleteException {
+    JUnitShell.getUnitTestShell().compileForWebMode(module,
+        JUnitShell.getRemoteUserAgents());
+  }
+
+}
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index 79833e1..4a198cd 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.junit;
 
+import com.google.gwt.junit.client.DevModeOnCompiledScriptTest;
 import com.google.gwt.junit.client.GWTTestCaseTest;
 import com.google.gwt.junit.client.PropertyDefiningGWTTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
@@ -33,6 +34,7 @@
     // Suppressed due to flakiness on Linux
     // suite.addTestSuite(BenchmarkTest.class);
     suite.addTestSuite(GWTTestCaseTest.class);
+    suite.addTestSuite(DevModeOnCompiledScriptTest.class);
 
     // Must run after a GWTTestCase so JUnitShell is initialized.
     suite.addTestSuite(BatchingStrategyTest.class);
diff --git a/user/test/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java b/user/test/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java
new file mode 100644
index 0000000..f9aefe8
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/DevModeOnCompiledScriptTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010 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.client;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.JUnitBridge;
+import com.google.gwt.junit.Platform;
+import com.google.gwt.junit.JUnitShell.Strategy;
+import com.google.gwt.junit.client.impl.JUnitResult;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests that we can run a test in dev mode even when the selection script is
+ * from a compile. Note that this is the VM-only version of the class; there is
+ * a translatable version for the client side.
+ */
+@DoNotRunWith(Platform.Prod)
+public class DevModeOnCompiledScriptTest extends GWTTestCase {
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.DevModeOnCompiledScriptTest";
+  }
+
+  /**
+   * GWT-unfriendly, forcing us to use a translatable class.
+   */
+  @Override
+  public Strategy getStrategy() {
+    final Strategy impl = super.getStrategy();
+    return new Strategy() {
+      public String getModuleInherit() {
+        return impl.getModuleInherit();
+      }
+
+      public String getSyntheticModuleExtension() {
+        return impl.getSyntheticModuleExtension();
+      }
+
+      public void processModule(ModuleDef module) {
+        impl.processModule(module);
+        try {
+          JUnitBridge.compileForWebMode(module);
+        } catch (UnableToCompleteException e) {
+          throw new RuntimeException("Failed to manually compile test module",
+              e);
+        }
+      }
+
+      public void processResult(TestCase testCase, JUnitResult result) {
+        impl.processResult(testCase, result);
+      }
+    };
+  }
+
+  public void testSomethingTrivial() {
+    assertTrue(true);
+  }
+}