Improve correctness of UnicodeEscapingTest.
Splits testClientToServerBMP() into two methods to correctly report failures.
Removes support for Safari2.
Extends WebKit regex to support Safari4.

Patch by: bobv
Review by: jat, jgw

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5666 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamWriter.java b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamWriter.java
index fe14d16..fe069cf 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamWriter.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamWriter.java
@@ -56,26 +56,18 @@
   private static native JavaScriptObject getQuotingRegex() /*-{
     // "|" = AbstractSerializationStream.RPC_SEPARATOR_CHAR
     var ua = navigator.userAgent.toLowerCase();
-    var webkitregex = /webkit\/([\d]+)/;
-    var webkit = 0;
-    var result = webkitregex.exec(ua);
-    if (result) {
-      webkit = parseInt(result[1]);
-    }
     if (ua.indexOf("android") != -1) {
       // initial version of Android WebKit has a double-encoding bug for UTF8,
       // so we have to encode every non-ASCII character.
       // TODO(jat): revisit when this bug is fixed in Android
       return /[\u0000\|\\\u0080-\uFFFF]/g;
-    } else if (webkit < 522) {
-      // Safari 2 doesn't handle \\uXXXX in regexes
-    // TODO(jat): should iPhone be treated specially?
-    return /[\x00\|\\]/g;
-    } else if (webkit > 0) {
-    // other WebKit-based browsers need some additional quoting
-    return /[\u0000\|\\\u0300-\u036F\u0590-\u05FF\uD800-\uFFFF]/g;
+    } else if (ua.indexOf("webkit") != -1) {
+      // other WebKit-based browsers need some additional quoting due to combining
+      // forms and normalization (one codepoint being replaced with another).
+      // Verified with Safari 4.0.1 (5530.18)
+      return /[\u0000\|\\\u0300-\u03ff\u0590-\u05FF\u0600-\u06ff\u0730-\u074A\u07eb-\u07f3\u0940-\u0963\u0980-\u09ff\u0a00-\u0a7f\u0b00-\u0b7f\u0e00-\u0e7f\u0f00-\u0fff\u1900-\u194f\u1a00-\u1a1f\u1b00-\u1b7f\u1dc0-\u1dff\u1f00-\u1fff\u2000-\u206f\u20d0-\u20ff\u2100-\u214f\u2300-\u23ff\u2a00-\u2aff\u3000-\u303f\uD800-\uFFFF]/g;
     } else {
-    return /[\u0000\|\\\uD800-\uFFFF]/g;
+      return /[\u0000\|\\\uD800-\uFFFF]/g;
     }
   }-*/;
 
diff --git a/user/test/com/google/gwt/user/client/rpc/UnicodeEscapingTest.java b/user/test/com/google/gwt/user/client/rpc/UnicodeEscapingTest.java
index f45777e..dcb8fbb 100644
--- a/user/test/com/google/gwt/user/client/rpc/UnicodeEscapingTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/UnicodeEscapingTest.java
@@ -19,11 +19,14 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.rpc.UnicodeEscapingService.InvalidCharacterException;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Test that any valid string can be sent via RPC in both directions.
  * 
- * TODO(jat): make unpaired surrogates work properly if it is possible to do
- * so on all browsers, then add them to this test.
+ * TODO(jat): make unpaired surrogates work properly if it is possible to do so
+ * on all browsers, then add them to this test.
  */
 public class UnicodeEscapingTest extends GWTTestCase {
 
@@ -32,8 +35,8 @@
 
   /**
    * When doing the non-BMP test, we don't test every block of characters
-   * because it takes too long - this is the increment to use.  It is not a
-   * power of two so we alter the alignment of the block of characters we skip.
+   * because it takes too long - this is the increment to use. It is not a power
+   * of two so we alter the alignment of the block of characters we skip.
    */
   private static final int NON_BMP_TEST_INCREMENT = 8192 + 64;
 
@@ -60,28 +63,6 @@
     return buf.toString();
   }
 
-  /*
-   * Copied from HistoryTest.
-   */
-  private static native boolean isSafari2() /*-{
-    var exp = / AppleWebKit\/([\d]+)/;
-    var result = exp.exec(navigator.userAgent);
-    if (result) {
-      // The standard history implementation works fine on WebKit >= 522
-      // (Safari 3 beta).
-      if (parseInt(result[1]) >= 522) {
-        return false;
-      }
-    }
-  
-    // The standard history implementation works just fine on the iPhone, which
-    // unfortunately reports itself as WebKit/420+.
-    if (navigator.userAgent.indexOf('iPhone') != -1) {
-      return false;
-    }
-  
-    return true;
-  }-*/;
   /**
    * Verifies that the supplied string includes the requested code points.
    * 
@@ -98,8 +79,8 @@
     }
     int expectedLen = end - start;
     int strLen = str.codePointCount(0, str.length());
-    for (int i = 0, codePoint = start; i < strLen;
-        i = Character.offsetByCodePoints(str, i, 1)) {
+    for (int i = 0, codePoint = start; i < strLen; i = Character.offsetByCodePoints(
+        str, i, 1)) {
       int strCodePoint = str.codePointAt(i);
       if (strCodePoint != codePoint) {
         throw new InvalidCharacterException(i, codePoint, strCodePoint);
@@ -109,14 +90,16 @@
     if (strLen < expectedLen) {
       throw new InvalidCharacterException(strLen, start + strLen, -1);
     } else if (expectedLen != strLen) {
-      throw new RuntimeException("Too many characters returned on block from U+"
-          + Integer.toHexString(start) + " to U+" + Integer.toHexString(end)
-          + ": expected=" + expectedLen + ", actual=" + strLen);
+      throw new RuntimeException(
+          "Too many characters returned on block from U+"
+              + Integer.toHexString(start) + " to U+"
+              + Integer.toHexString(end) + ": expected=" + expectedLen
+              + ", actual=" + strLen);
     }
   }
+
   private static UnicodeEscapingServiceAsync getService() {
-    UnicodeEscapingServiceAsync service = GWT.create(
-        UnicodeEscapingService.class);
+    UnicodeEscapingServiceAsync service = GWT.create(UnicodeEscapingService.class);
     ServiceDefTarget target = (ServiceDefTarget) service;
     target.setServiceEntryPoint(GWT.getModuleBaseURL() + "unicodeEscape");
     return service;
@@ -136,30 +119,41 @@
    * properly handles all BMP characters.
    * 
    * Unpaired or improperly paired surrogates are not tested here, as some
-   * browsers refuse to accept them.  Properly paired surrogates are tested
-   * in the non-BMP test.
-   *  
+   * browsers refuse to accept them. Properly paired surrogates are tested in
+   * the non-BMP test.
+   * 
    * Note that this does not test all possible combinations, which might be an
    * issue, particularly with combining marks, though they should be logically
    * equivalent in that case.
    * 
    * @throws InvalidCharacterException
    */
-  public void testClientToServerBMP() throws InvalidCharacterException {
+  public void testClientToServerBMPHigh() throws InvalidCharacterException {
     delayTestFinish(TEST_FINISH_DELAY_MS);
-    if (isSafari2()) {
-      // Safari2 can't be fixed for many characters, including null
-      // We only guarantee that basic ISO-Latin characters are unmolested.
-      clientToServerVerifyRange(0x0001, 0x0300, CHARACTER_BLOCK_SIZE,
-          CHARACTER_BLOCK_SIZE);
-    } else {
-      clientToServerVerifyRange(Character.MIN_CODE_POINT,
-          Character.MIN_SURROGATE, CHARACTER_BLOCK_SIZE,
-          CHARACTER_BLOCK_SIZE);
-      clientToServerVerifyRange(Character.MAX_SURROGATE + 1,
-          Character.MIN_SUPPLEMENTARY_CODE_POINT, CHARACTER_BLOCK_SIZE,
-          CHARACTER_BLOCK_SIZE);
-    }
+    clientToServerVerifyRange(Character.MAX_SURROGATE + 1,
+        Character.MIN_SUPPLEMENTARY_CODE_POINT, CHARACTER_BLOCK_SIZE,
+        CHARACTER_BLOCK_SIZE);
+  }
+
+  /**
+   * Generate strings containing ranges of characters and sends them to the
+   * server for verification. This ensures that client->server string escaping
+   * properly handles all BMP characters.
+   * 
+   * Unpaired or improperly paired surrogates are not tested here, as some
+   * browsers refuse to accept them. Properly paired surrogates are tested in
+   * the non-BMP test.
+   * 
+   * Note that this does not test all possible combinations, which might be an
+   * issue, particularly with combining marks, though they should be logically
+   * equivalent in that case.
+   * 
+   * @throws InvalidCharacterException
+   */
+  public void testClientToServerBMPLow() throws InvalidCharacterException {
+    delayTestFinish(TEST_FINISH_DELAY_MS);
+    clientToServerVerifyRange(Character.MIN_CODE_POINT,
+        Character.MIN_SURROGATE, CHARACTER_BLOCK_SIZE, CHARACTER_BLOCK_SIZE);
   }
 
   /**
@@ -181,9 +175,9 @@
   }
 
   /**
-   * Requests strings of CHARACTER_RANGE_SIZE from the server and validates
-   * that the returned string length matches CHARACTER_RANGE_SIZE and that all
-   * of the characters remain intact.
+   * Requests strings of CHARACTER_RANGE_SIZE from the server and validates that
+   * the returned string length matches CHARACTER_RANGE_SIZE and that all of the
+   * characters remain intact.
    * 
    * Note that this does not test all possible combinations, which might be an
    * issue, particularly with combining marks, though they should be logically
@@ -199,7 +193,7 @@
   /**
    * Requests strings of CHARACTER_RANGE_SIZE from the server and validates that
    * the returned string length matches CHARACTER_RANGE_SIZE and that all of the
-   * characters remain intact.  Note that this test verifies non-BMP characters
+   * characters remain intact. Note that this test verifies non-BMP characters
    * (ie, those which are represented as pairs of surrogates).
    * 
    * Note that this does not test all possible combinations, which might be an
@@ -220,53 +214,78 @@
     getService().verifyStringContainingCharacterRange(current, blockEnd,
         getStringContainingCharacterRange(start, blockEnd),
         new AsyncCallback<Boolean>() {
-      public void onFailure(Throwable caught) {
-        TestSetValidator.rethrowException(caught);
-      }
+          List<Throwable> fails = new ArrayList<Throwable>();
 
-      public void onSuccess(Boolean ignored) {
-        current += step;
-        if (current < end) {
-          delayTestFinish(TEST_FINISH_DELAY_MS);
-          int blockEnd = Math.min(end, current + size);
-          try {
-            getService().verifyStringContainingCharacterRange(current, blockEnd,
-                getStringContainingCharacterRange(current, blockEnd), this);
-          } catch (InvalidCharacterException e) {
-            TestSetValidator.rethrowException(e);
+          public void onFailure(Throwable caught) {
+            fails.add(caught);
+            onSuccess(false);
           }
-        } else {
-          finishTest();
-        }
-      }
-    });
+
+          public void onSuccess(Boolean ignored) {
+            current += step;
+            if (current < end) {
+              delayTestFinish(TEST_FINISH_DELAY_MS);
+              int blockEnd = Math.min(end, current + size);
+              try {
+                getService().verifyStringContainingCharacterRange(current,
+                    blockEnd,
+                    getStringContainingCharacterRange(current, blockEnd), this);
+              } catch (InvalidCharacterException e) {
+                fails.add(e);
+              }
+            } else if (!fails.isEmpty()) {
+              StringBuilder msg = new StringBuilder();
+              for (Throwable t : fails) {
+                msg.append(t.getMessage()).append("\n");
+              }
+              TestSetValidator.rethrowException(new RuntimeException(
+                  msg.toString()));
+            } else {
+              finishTest();
+            }
+          }
+        });
   }
 
   private void serverToClientVerify(final int start, final int end,
       final int size, final int step) {
     current = start;
-    getService().getStringContainingCharacterRange(start, Math.min(end,
-        current + size), new AsyncCallback<String>() {
-      public void onFailure(Throwable caught) {
-        TestSetValidator.rethrowException(caught);
-      }
+    getService().getStringContainingCharacterRange(start,
+        Math.min(end, current + size), new AsyncCallback<String>() {
+          List<Throwable> fails = new ArrayList<Throwable>();
 
-      public void onSuccess(String str) {
-        try {
-          verifyStringContainingCharacterRange(current, Math.min(end,
-              current + size), str);
-        } catch (InvalidCharacterException e) {
-          TestSetValidator.rethrowException(e);
-        }
-        current += step;
-        if (current < end) {
-          delayTestFinish(TEST_FINISH_DELAY_MS);
-          getService().getStringContainingCharacterRange(current,
-              Math.min(end, current + size), this);
-        } else {
-          finishTest();
-        }
-      }
-    });
+          public void onFailure(Throwable caught) {
+            fails.add(caught);
+            nextBatch();
+          }
+
+          public void onSuccess(String str) {
+            try {
+              verifyStringContainingCharacterRange(current, Math.min(end,
+                  current + size), str);
+            } catch (InvalidCharacterException e) {
+              fails.add(e);
+            }
+            nextBatch();
+          }
+
+          private void nextBatch() {
+            current += step;
+            if (current < end) {
+              delayTestFinish(TEST_FINISH_DELAY_MS);
+              getService().getStringContainingCharacterRange(current,
+                  Math.min(end, current + size), this);
+            } else if (!fails.isEmpty()) {
+              StringBuilder msg = new StringBuilder();
+              for (Throwable t : fails) {
+                msg.append(t.getMessage()).append("\n");
+              }
+              TestSetValidator.rethrowException(new RuntimeException(
+                  msg.toString()));
+            } else {
+              finishTest();
+            }
+          }
+        });
   }
 }