Test the scenario where GWT.runAsync() fails to load a code fragment due to a
server or network problem.  We use a custom servlet and linker to inject the
errors.

Review by: spoon



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5286 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index b63b22f..e11d31b 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -93,6 +93,15 @@
     return ".cache.html";
   }
 
+  /**
+   * Returns the subdirectory name to be used by getModulPrefix when requesting
+   * a runAsync module. The default implementation returns the value of
+   * FRAGMENT_SUDBIR. This has been factored out for test cases.
+   */
+  protected String getFragmentSubdir() {
+    return FRAGMENT_SUBDIR;
+  }
+
   @Override
   protected String getModulePrefix(TreeLogger logger, LinkerContext context,
       String strongName) {
@@ -123,7 +132,7 @@
 
     return out.toString();
   }
-
+  
   @Override
   protected String getSelectionScriptTemplate(TreeLogger logger,
       LinkerContext context) {
@@ -157,7 +166,7 @@
       out.print("function __gwtStartLoadingFragment(frag) {");
       out.indentIn();
       out.newlineOpt();
-      out.print("  return $moduleBase + '" + FRAGMENT_SUBDIR
+      out.print("  return $moduleBase + '" + getFragmentSubdir()
           + "/'  + $strongName + '/' + frag + '" + FRAGMENT_EXTENSION + "';");
       out.indentOut();
       out.newlineOpt();
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
index 9f0c603..db89ae3 100644
--- a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
@@ -39,6 +39,7 @@
 import com.google.gwt.dev.jjs.test.MiscellaneousTest;
 import com.google.gwt.dev.jjs.test.NativeLongTest;
 import com.google.gwt.dev.jjs.test.ObjectIdentityTest;
+import com.google.gwt.dev.jjs.test.RunAsyncFailureTest;
 import com.google.gwt.dev.jjs.test.RunAsyncTest;
 import com.google.gwt.dev.jjs.test.SingleJsoImplTest;
 import com.google.gwt.dev.jjs.test.UnstableGeneratorTest;
@@ -80,6 +81,7 @@
     suite.addTestSuite(MiscellaneousTest.class);
     suite.addTestSuite(NativeLongTest.class);
     suite.addTestSuite(ObjectIdentityTest.class);
+    suite.addTestSuite(RunAsyncFailureTest.class);
     suite.addTestSuite(RunAsyncTest.class);
     suite.addTestSuite(SingleJsoImplTest.class);
     suite.addTestSuite(UnstableGeneratorTest.class);
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
new file mode 100644
index 0000000..29935b6
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
@@ -0,0 +1,22 @@
+<!--                                                                        -->
+<!-- Copyright 2009 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>
+    <source path='test' />
+    <servlet path="/runAsyncFailure"
+        class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet"/>
+    <define-linker name="runAsyncFailure"
+        class="com.google.gwt.user.server.runasync.RunAsyncFailureIFrameLinker"/>
+    <add-linker name="runAsyncFailure"/>
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
new file mode 100644
index 0000000..bbf7b92
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2009 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.dev.jjs.test;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Timer;
+
+/**
+ * Tests runAsync server/network failure handling.
+ */
+public class RunAsyncFailureTest extends GWTTestCase {
+
+  abstract static class MyRunAsyncCallback implements RunAsyncCallback {
+    private static int sToken = 0;
+    private int token;
+    
+    public MyRunAsyncCallback() {
+      token = sToken++;
+    }
+    
+    public int getToken() {
+      return token;
+    }
+  }
+
+  private static final int RUNASYNC_TIMEOUT = 30000;
+  
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.RunAsyncFailure";
+  }
+  
+  // Repeated runAsync using a Timer
+  public void runAsync1(final int attempt) {
+    log("runAsync1: attempt = " + attempt);
+    GWT.runAsync(new MyRunAsyncCallback() {
+      public void onFailure(Throwable caught) {
+        // Fail the test if too many attempts have taken place.
+        if (attempt > 20) {
+          fail();
+        }
+        
+        int token = getToken();
+        log("onFailure: attempt = " + attempt + ", token = " + token
+            + ", caught = " + caught);
+        new Timer() {
+          @Override
+          public void run() {
+            runAsync1(attempt + 1);
+          }
+        }.schedule(100);
+      }
+
+      public void onSuccess() {
+        int token = getToken();
+        log("onSuccess: attempt = " + attempt + ", token = " + token);
+        finishTest();
+      }
+    });
+  }
+  
+  /**
+   * Test the basic functionality of retrying runAsync until is succeeds.
+   * A Timer is used to avoid nesting GWT.runAsync calls.
+   */
+  public void testHttpFailureRetries() {
+    delayTestFinish(RUNASYNC_TIMEOUT);
+    runAsync1(0);
+  }
+
+  private native void log(String message) /*-{
+    $wnd.console.log(message);
+  }-*/;
+}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
new file mode 100644
index 0000000..33c5acc
--- /dev/null
+++ b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2009 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.user.server.runasync;
+
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.core.linker.IFrameLinker;
+
+/**
+ * Load modules from a custom servlet in order to test download failure
+ * behavior.
+ */
+@LinkerOrder(Order.PRE)
+public class RunAsyncFailureIFrameLinker extends IFrameLinker {
+
+  @Override
+  protected String getFragmentSubdir() {
+    return "runAsyncFailure/" + FRAGMENT_SUBDIR;
+  }
+}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
new file mode 100644
index 0000000..a4a28e5
--- /dev/null
+++ b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2009 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.user.server.runasync;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet for {@link com.google.gwt.dev.jjs.test.RunAsyncFailureTest}.
+ */
+public class RunAsyncFailureServlet extends HttpServlet {
+  
+  private static final boolean DEBUG = false;
+  private static final HashSet<String> errorFragments = new HashSet<String>();
+  private static final int[] responses = {
+      HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+      HttpServletResponse.SC_GATEWAY_TIMEOUT,
+      HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+      HttpServletResponse.SC_GATEWAY_TIMEOUT,
+      HttpServletResponse.SC_OK // must be last
+  };
+  
+  static {
+    errorFragments.add("2.cache.js");
+  }
+  
+  private static void debug(String message) {
+    if (DEBUG) {
+      System.out.println(message);
+    }
+  }
+  
+  HashMap<String,Integer> triesMap = new HashMap<String,Integer>();
+  
+  private int sSerial = 0;
+  
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    String originalUri = req.getRequestURI();
+    debug("doGet: " + originalUri);
+    String uri = originalUri.replace("/runAsyncFailure", "");
+    
+    int response = getDesiredResponse(uri);
+    String fragment = uri.substring(uri.lastIndexOf('/') + 1);
+    if (!errorFragments.contains(fragment)
+        || response == HttpServletResponse.SC_OK) {
+      // Delegate the actual data fetch to the main servlet
+      String host = req.getLocalName();
+      int port = req.getLocalPort();
+      String realUrl = "http://" + host + ":" + port + uri;
+      debug("Fetching: " + realUrl);
+      
+      int bytes = 0;
+      try {
+        URL url = new URL(realUrl);
+        InputStream is = url.openStream();
+        OutputStream os = resp.getOutputStream();
+
+        byte[] data = new byte[8192];
+        int nbytes;
+        while ((nbytes = is.read(data)) != -1) {
+          os.write(data, 0, nbytes);
+          bytes += nbytes;
+        }
+        is.close();
+        os.close();
+      } catch (IOException e) {
+        debug("IOException fetching real data: " + e);
+        throw e;
+      }
+      
+      resp.setHeader("Cache-Control", "no-cache");
+      resp.setContentLength(bytes);
+      resp.setContentType("text/html");
+      resp.setStatus(HttpServletResponse.SC_OK);
+      
+      debug("doGet: served " + uri + " (" + bytes + " bytes)");
+    } else {
+      resp.setHeader("Cache-Control", "no-cache");
+      resp.sendError(response, "serial=" + getNextSerial());
+      
+      debug("doGet: sent error " + response + " for " + uri);
+    }
+  }
+  
+  private int getDesiredResponse(String resource) {
+    Integer t = triesMap.get(resource);
+    int tries = t == null ? 0 : t.intValue();
+    triesMap.put(resource, tries + 1);
+    
+    return responses[tries % responses.length];
+  }
+  
+  private synchronized int getNextSerial() {
+    return sSerial++;
+  }
+}