Link backing errors together with a cause attribute

We currently only link backingJsObjects for Throwables bidirectionally between
the JS object and the Java Throwable. We then only chain the Throwable caused
bys. However, there are situtation where it would be useful to have the JS
errors linked together, in particular when an uncaught error gets handled. We
could traverse back and forth between the Java Throwable and the backing object,
but that's not always feasible (ex. when running in v8/jsc directly and the
error object gets handled on the native side).

Author: kevinoconnor
Change-Id: I43102d3e9796ac307a4acdfff46263e903106daf
diff --git a/user/super/com/google/gwt/emul/java/lang/Throwable.java b/user/super/com/google/gwt/emul/java/lang/Throwable.java
index 2a1bc67..fb7eddd 100644
--- a/user/super/com/google/gwt/emul/java/lang/Throwable.java
+++ b/user/super/com/google/gwt/emul/java/lang/Throwable.java
@@ -22,6 +22,7 @@
 import java.io.PrintStream;
 import java.io.Serializable;
 
+import javaemul.internal.JsUtils;
 import javaemul.internal.annotations.DoNotInline;
 
 import jsinterop.annotations.JsMethod;
@@ -146,6 +147,7 @@
   private void setBackingJsObject(Object backingJsObject) {
     this.backingJsObject = backingJsObject;
     linkBack(backingJsObject);
+    linkBackingCause();
   }
 
   private void linkBack(Object error) {
@@ -157,6 +159,13 @@
     }
   }
 
+  private void linkBackingCause() {
+    if (cause == null || !(backingJsObject instanceof NativeError)) {
+      return;
+    }
+    JsUtils.setProperty(backingJsObject, "cause", cause.backingJsObject);
+  }
+
   /**
    * Call to add an exception that was suppressed. Used by try-with-resources.
    */
@@ -242,6 +251,7 @@
     checkState(this.cause == null, "Can't overwrite cause");
     checkCriticalArgument(cause != this, "Self-causation not permitted");
     this.cause = cause;
+    linkBackingCause();
     return this;
   }
 
diff --git a/user/test/com/google/gwt/core/client/impl/StackTraceEmulTest.java b/user/test/com/google/gwt/core/client/impl/StackTraceEmulTest.java
index b15ce75..97e8978 100644
--- a/user/test/com/google/gwt/core/client/impl/StackTraceEmulTest.java
+++ b/user/test/com/google/gwt/core/client/impl/StackTraceEmulTest.java
@@ -67,7 +67,7 @@
     String[] methodNames = getTraceJava();
 
     StackTraceElement[] expectedTrace = new StackTraceElement[] {
-        createSTE(methodNames[0], "Throwable.java", 68),
+        createSTE(methodNames[0], "Throwable.java", 69),
         createSTE(methodNames[1], "Exception.java", 29),
         createSTE(methodNames[2], "StackTraceExamples.java", 57),
         createSTE(methodNames[3], "StackTraceExamples.java", 52),
diff --git a/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java b/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
index 34b7da0..820eb3a 100644
--- a/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
+++ b/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
@@ -17,11 +17,11 @@
 
 import com.google.gwt.testing.TestUtils;
 
+import javaemul.internal.JsUtils;
+
 import jsinterop.annotations.JsType;
 
-/**
- * Unit tests for the GWT emulation of java.lang.Throwable class.
- */
+/** Unit tests for the GWT emulation of java.lang.Throwable class. */
 public class ThrowableTest extends ThrowableTestBase {
 
   @Override
@@ -113,6 +113,48 @@
     assertSame(e, javaNativeJavaSandwich(e));
   }
 
+  public void testLinkedBackingObjects() {
+    if (TestUtils.isJvm()) {
+      return;
+    }
+    Throwable rootCause = new Throwable("Root cause");
+    Throwable subError = new Throwable("Sub-error", rootCause);
+
+    Error backingError = (Error) catchNative(createThrower(subError));
+    Error rootBackingError = (Error) catchNative(createThrower(rootCause));
+    assertEquals(
+        "backingJsObject should have a cause linked to the parent backingJsObject",
+        rootBackingError,
+        JsUtils.getProperty(backingError, "cause"));
+  }
+
+  public void testLinkedBackingObjects_initCause() {
+    if (TestUtils.isJvm()) {
+      return;
+    }
+    Throwable rootCause = new Throwable("Root cause");
+    Throwable subError = new Throwable("Sub-error");
+    subError.initCause(rootCause);
+
+    Error backingError = (Error) catchNative(createThrower(subError));
+    Error rootBackingError = (Error) catchNative(createThrower(rootCause));
+    assertEquals(
+        "backingJsObject should have a cause linked to the parent backingJsObject",
+        rootBackingError,
+        JsUtils.getProperty(backingError, "cause"));
+  }
+
+  public void testLinkedBackingObjects_noCause() {
+    if (TestUtils.isJvm()) {
+      return;
+    }
+    Throwable subError = new Throwable("Sub-error");
+
+    Error backingError = (Error) catchNative(createThrower(subError));
+    assertNull(
+        "backingJsObject should have no linked cause", JsUtils.getProperty(backingError, "cause"));
+  }
+
   @JsType(isNative = true, namespace = "<window>")
   private static class Error { }
 }