Merge "Fix race condition in console tests" into androidx-main
diff --git a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
index 052b00f..553e919 100644
--- a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
+++ b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 
+import androidx.annotation.GuardedBy;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -33,6 +34,7 @@
 
 import java.nio.charset.StandardCharsets;
 import java.util.Vector;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
@@ -938,25 +940,46 @@
     }
 
     @Test
-    @MediumTest
+    @LargeTest
     public void testConsoleLogging() throws Throwable {
         final class LoggingJavaScriptConsoleCallback implements JavaScriptConsoleCallback {
-            private StringBuilder mMessages = new StringBuilder();
+            private final Object mLock = new Object();
+            @GuardedBy("mLock")
+            private final StringBuilder mMessages = new StringBuilder();
 
             public static final String CLEAR_MARKER = "(console.clear() called)\n";
+            // This is required for synchronization between the instrumentation thread and the UI
+            // thread.
+            public CountDownLatch latch;
+
+            LoggingJavaScriptConsoleCallback(int numberOfCalls) {
+                latch = new CountDownLatch(numberOfCalls);
+            }
 
             @Override
             public void onConsoleMessage(JavaScriptConsoleCallback.ConsoleMessage message) {
-                mMessages.append(message.toString()).append("\n");
+                synchronized (mLock) {
+                    mMessages.append(message.toString()).append("\n");
+                }
+                latch.countDown();
             }
 
             @Override
             public void onConsoleClear() {
-                mMessages.append(CLEAR_MARKER);
+                synchronized (mLock) {
+                    mMessages.append(CLEAR_MARKER);
+                }
+                latch.countDown();
             }
 
             public String messages() {
-                return mMessages.toString();
+                synchronized (mLock) {
+                    return mMessages.toString();
+                }
+            }
+
+            public void resetLatch(int count) {
+                latch = new CountDownLatch(count);
             }
         }
 
@@ -989,16 +1012,19 @@
                 + "D <expression>:12:9: I am counting: 1\n"
                 + "D <expression>:13:9: I am counting: 2\n"
                 + LoggingJavaScriptConsoleCallback.CLEAR_MARKER;
+        final int numOfLogs = 11;
         final Context context = ApplicationProvider.getApplicationContext();
 
         final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
                 JavaScriptSandbox.createConnectedInstanceAsync(context);
         try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
-                JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
+             JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
             Assume.assumeTrue(jsSandbox.isFeatureSupported(
                     JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING));
-            final LoggingJavaScriptConsoleCallback client1 = new LoggingJavaScriptConsoleCallback();
-            final LoggingJavaScriptConsoleCallback client2 = new LoggingJavaScriptConsoleCallback();
+            final LoggingJavaScriptConsoleCallback client1 =
+                    new LoggingJavaScriptConsoleCallback(numOfLogs);
+            final LoggingJavaScriptConsoleCallback client2 =
+                    new LoggingJavaScriptConsoleCallback(numOfLogs);
             // Test logging does not crash when no client attached
             // (There may be no inspector)
             {
@@ -1017,6 +1043,7 @@
                 final String result = resultFuture.get(5, TimeUnit.SECONDS);
 
                 Assert.assertEquals(expected, result);
+                Assert.assertTrue(client1.latch.await(2, TimeUnit.SECONDS));
                 Assert.assertEquals(expectedLog, client1.messages());
             }
             // Test client can be replaced
@@ -1028,9 +1055,8 @@
                 final String result = resultFuture.get(5, TimeUnit.SECONDS);
 
                 Assert.assertEquals(expected, result);
+                Assert.assertTrue(client2.latch.await(2, TimeUnit.SECONDS));
                 Assert.assertEquals(expectedLog, client2.messages());
-                // Ensure client1 hasn't received anything additional
-                Assert.assertEquals(expectedLog, client1.messages());
             }
             // Test client can be nullified/disabled
             // (This may tear down the inspector)
@@ -1041,21 +1067,24 @@
                 final String result = resultFuture.get(5, TimeUnit.SECONDS);
 
                 Assert.assertEquals(expected, result);
-                // Ensure clients haven't received anything additional
-                Assert.assertEquals(expectedLog, client1.messages());
-                Assert.assertEquals(expectedLog, client2.messages());
             }
             // Ensure console messaging can be re-enabled (on an existing client)
             // (This may spin up a new inspector)
             {
+                client1.resetLatch(numOfLogs);
                 jsIsolate.setConsoleCallback(client1);
                 final ListenableFuture<String> resultFuture =
                         jsIsolate.evaluateJavaScriptAsync(code);
                 final String result = resultFuture.get(5, TimeUnit.SECONDS);
 
                 Assert.assertEquals(expected, result);
+                Assert.assertTrue(client1.latch.await(2, TimeUnit.SECONDS));
                 Assert.assertEquals(expectedLog + expectedLog, client1.messages());
-                // Ensure client2 hasn't received anything additional
+            }
+            // Ensure client1 and client2 hasn't received anything additional
+            {
+                Thread.sleep(1000);
+                Assert.assertEquals(expectedLog + expectedLog, client1.messages());
                 Assert.assertEquals(expectedLog, client2.messages());
             }
         }
@@ -1085,26 +1114,28 @@
         final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
                 JavaScriptSandbox.createConnectedInstanceAsync(context);
         try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
-                JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
+             JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
             Assume.assumeTrue(jsSandbox.isFeatureSupported(
                     JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING));
             Assume.assumeTrue(
                     jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
 
-            jsIsolate.setConsoleCallback(new JavaScriptConsoleCallback(){
-                    @Override
-                    public void onConsoleMessage(ConsoleMessage message) {}
+            CountDownLatch latch = new CountDownLatch(1);
+            jsIsolate.setConsoleCallback(new JavaScriptConsoleCallback() {
+                @Override
+                public void onConsoleMessage(ConsoleMessage message) {}
 
-                    @Override
-                    public void onConsoleClear() {
-                        jsIsolate.evaluateJavaScriptAsync(callbackCode);
-                    }
-                });
+                @Override
+                public void onConsoleClear() {
+                    jsIsolate.evaluateJavaScriptAsync(callbackCode);
+                    latch.countDown();
+                }
+            });
             final ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
             // Note: the main executor is on a different thread to the instrumentation thread, so
             // blocking here will not block the console callback.
             final String result = resultFuture.get(5, TimeUnit.SECONDS);
-
+            Assert.assertTrue(latch.await(2, TimeUnit.SECONDS));
             Assert.assertEquals(expected, result);
         }
     }