1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.js;
18 
19 import static com.android.adservices.service.js.JSScriptArgument.arrayArg;
20 import static com.android.adservices.service.js.JSScriptArgument.numericArg;
21 import static com.android.adservices.service.js.JSScriptArgument.recordArg;
22 import static com.android.adservices.service.js.JSScriptArgument.stringArg;
23 import static com.android.adservices.service.js.JSScriptEngine.JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG;
24 import static com.android.adservices.service.js.JSScriptEngine.JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG;
25 
26 import static com.google.common.truth.Truth.assertThat;
27 
28 import static org.junit.Assert.assertEquals;
29 import static org.junit.Assert.assertThrows;
30 import static org.junit.Assert.assertTrue;
31 import static org.junit.Assume.assumeFalse;
32 import static org.junit.Assume.assumeTrue;
33 import static org.mockito.ArgumentMatchers.any;
34 import static org.mockito.ArgumentMatchers.anyString;
35 import static org.mockito.Mockito.atLeastOnce;
36 import static org.mockito.Mockito.doAnswer;
37 import static org.mockito.Mockito.doNothing;
38 import static org.mockito.Mockito.doThrow;
39 import static org.mockito.Mockito.mock;
40 import static org.mockito.Mockito.never;
41 import static org.mockito.Mockito.reset;
42 import static org.mockito.Mockito.timeout;
43 import static org.mockito.Mockito.verify;
44 import static org.mockito.Mockito.when;
45 
46 import android.content.Context;
47 
48 import androidx.annotation.NonNull;
49 import androidx.javascriptengine.IsolateStartupParameters;
50 import androidx.javascriptengine.JavaScriptConsoleCallback;
51 import androidx.javascriptengine.JavaScriptIsolate;
52 import androidx.javascriptengine.JavaScriptSandbox;
53 import androidx.javascriptengine.MemoryLimitExceededException;
54 import androidx.javascriptengine.SandboxDeadException;
55 import androidx.test.core.app.ApplicationProvider;
56 import androidx.test.filters.SmallTest;
57 
58 import com.android.adservices.LoggerFactory;
59 import com.android.adservices.service.common.NoOpRetryStrategyImpl;
60 import com.android.adservices.service.common.RetryStrategy;
61 import com.android.adservices.service.common.RetryStrategyImpl;
62 import com.android.adservices.service.exception.JSExecutionException;
63 import com.android.adservices.service.profiling.JSScriptEngineLogConstants;
64 import com.android.adservices.service.profiling.Profiler;
65 import com.android.adservices.service.profiling.StopWatch;
66 import com.android.adservices.shared.testing.SdkLevelSupportRule;
67 import com.android.dx.mockito.inline.extended.ExtendedMockito;
68 import com.android.modules.utils.build.SdkLevel;
69 
70 import com.google.common.collect.ImmutableList;
71 import com.google.common.io.ByteStreams;
72 import com.google.common.util.concurrent.FluentFuture;
73 import com.google.common.util.concurrent.Futures;
74 import com.google.common.util.concurrent.ListenableFuture;
75 import com.google.common.util.concurrent.ListeningExecutorService;
76 import com.google.common.util.concurrent.MoreExecutors;
77 
78 import org.junit.Assume;
79 import org.junit.Before;
80 import org.junit.BeforeClass;
81 import org.junit.Rule;
82 import org.junit.Test;
83 import org.junit.function.ThrowingRunnable;
84 import org.mockito.Mock;
85 import org.mockito.Mockito;
86 import org.mockito.MockitoAnnotations;
87 import org.mockito.MockitoSession;
88 import org.mockito.quality.Strictness;
89 
90 import java.io.IOException;
91 import java.io.InputStream;
92 import java.util.Arrays;
93 import java.util.List;
94 import java.util.Objects;
95 import java.util.concurrent.CountDownLatch;
96 import java.util.concurrent.ExecutionException;
97 import java.util.concurrent.ExecutorService;
98 import java.util.concurrent.Executors;
99 import java.util.concurrent.ScheduledThreadPoolExecutor;
100 import java.util.concurrent.TimeUnit;
101 import java.util.concurrent.TimeoutException;
102 import java.util.concurrent.atomic.AtomicBoolean;
103 import java.util.stream.Collectors;
104 
105 @SmallTest
106 public class JSScriptEngineTest {
107 
108     /**
109      * functions in simple_test_functions.wasm:
110      *
111      * <p>int increment(int n) { return n+1; }
112      *
113      * <p>int fib(int n) { if (n<=1) { return n; } else { return fib(n-2) + fib(n-1); } }
114      *
115      * <p>int fact(int n) { if (n<=1) { return 1; } else { return n * fact(n-1); } }
116      *
117      * <p>double log_base_2(double n) { return log(n) / log(2.0); }
118      */
119     public static final String WASM_MODULE = "simple_test_functions.wasm";
120 
121     private static final String TAG = JSScriptEngineTest.class.getSimpleName();
122 
123     protected static final Context sContext = ApplicationProvider.getApplicationContext();
124     private static final Profiler sMockProfiler = mock(Profiler.class);
125     private static final StopWatch sSandboxInitWatch = mock(StopWatch.class);
126     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
127     private static JSScriptEngine sJSScriptEngine;
128     private final ExecutorService mExecutorService = Executors.newFixedThreadPool(10);
129     private final boolean mDefaultIsolateConsoleMessageInLogs = false;
130     private final IsolateSettings mDefaultIsolateSettings =
131             IsolateSettings.forMaxHeapSizeEnforcementDisabled(mDefaultIsolateConsoleMessageInLogs);
132     private final RetryStrategy mNoOpRetryStrategy = new NoOpRetryStrategyImpl();
133     @Mock JSScriptEngine.JavaScriptSandboxProvider mMockSandboxProvider;
134     @Mock private StopWatch mIsolateCreateWatch;
135     @Mock private StopWatch mJavaExecutionWatch;
136     @Mock private JavaScriptSandbox mMockedSandbox;
137     @Mock private JavaScriptIsolate mMockedIsolate;
138 
139     @Rule(order = 0)
140     public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
141 
142     @BeforeClass
initJavaScriptSandbox()143     public static void initJavaScriptSandbox() {
144         when(sMockProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
145                 .thenReturn(sSandboxInitWatch);
146         doNothing().when(sSandboxInitWatch).stop();
147         if (JSScriptEngine.AvailabilityChecker.isJSSandboxAvailable()) {
148             sJSScriptEngine =
149                     JSScriptEngine.getInstanceForTesting(sContext, sMockProfiler, sLogger);
150         }
151     }
152 
153     @Before
setup()154     public void setup() {
155         Assume.assumeTrue(JSScriptEngine.AvailabilityChecker.isJSSandboxAvailable());
156         MockitoAnnotations.initMocks(this);
157 
158         reset(sMockProfiler);
159         when(sMockProfiler.start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME))
160                 .thenReturn(mIsolateCreateWatch);
161         when(sMockProfiler.start(JSScriptEngineLogConstants.JAVA_EXECUTION_TIME))
162                 .thenReturn(mJavaExecutionWatch);
163 
164         FluentFuture<JavaScriptSandbox> futureInstance =
165                 FluentFuture.from(Futures.immediateFuture(mMockedSandbox));
166         when(mMockSandboxProvider.getFutureInstance(sContext)).thenReturn(futureInstance);
167     }
168 
169     @Test
testProviderFailsIfJSSandboxNotAvailableInWebViewVersion()170     public void testProviderFailsIfJSSandboxNotAvailableInWebViewVersion() {
171         MockitoSession staticMockSessionLocal = null;
172 
173         try {
174             staticMockSessionLocal =
175                     ExtendedMockito.mockitoSession()
176                             .spyStatic(JavaScriptSandbox.class)
177                             .strictness(Strictness.LENIENT)
178                             .initMocks(this)
179                             .startMocking();
180             ExtendedMockito.doReturn(false).when(JavaScriptSandbox::isSupported);
181 
182             ThrowingRunnable getFutureInstance =
183                     () ->
184                             new JSScriptEngine.JavaScriptSandboxProvider(sMockProfiler, sLogger)
185                                     .getFutureInstance(sContext)
186                                     .get();
187 
188             Exception futureException = assertThrows(ExecutionException.class, getFutureInstance);
189             assertThat(futureException)
190                     .hasCauseThat()
191                     .isInstanceOf(JSSandboxIsNotAvailableException.class);
192         } finally {
193             if (staticMockSessionLocal != null) {
194                 staticMockSessionLocal.finishMocking();
195             }
196         }
197     }
198 
199     @Test
testEngineFailsIfJSSandboxNotAvailableInWebViewVersion()200     public void testEngineFailsIfJSSandboxNotAvailableInWebViewVersion() {
201         MockitoSession staticMockSessionLocal = null;
202 
203         try {
204             staticMockSessionLocal =
205                     ExtendedMockito.mockitoSession()
206                             .spyStatic(JavaScriptSandbox.class)
207                             .strictness(Strictness.LENIENT)
208                             .initMocks(this)
209                             .startMocking();
210             ExtendedMockito.doReturn(false).when(JavaScriptSandbox::isSupported);
211 
212             ThrowingRunnable getFutureInstance =
213                     () ->
214                             callJSEngine(
215                                     JSScriptEngine.createNewInstanceForTesting(
216                                             sContext,
217                                             new JSScriptEngine.JavaScriptSandboxProvider(
218                                                     sMockProfiler, sLogger),
219                                             sMockProfiler,
220                                             sLogger),
221                                     "function test() { return \"hello world\"; }",
222                                     ImmutableList.of(),
223                                     "test",
224                                     mDefaultIsolateSettings,
225                                     mNoOpRetryStrategy);
226 
227             Exception futureException = assertThrows(ExecutionException.class, getFutureInstance);
228             assertThat(futureException)
229                     .hasCauseThat()
230                     .isInstanceOf(JSSandboxIsNotAvailableException.class);
231         } finally {
232             if (staticMockSessionLocal != null) {
233                 staticMockSessionLocal.finishMocking();
234             }
235         }
236     }
237 
238     @Test
testCanRunSimpleScriptWithNoArgs()239     public void testCanRunSimpleScriptWithNoArgs() throws Exception {
240         assertThat(
241                         callJSEngine(
242                                 "function test() { return \"hello world\"; }",
243                                 ImmutableList.of(),
244                                 "test",
245                                 mDefaultIsolateSettings,
246                                 mNoOpRetryStrategy))
247                 .isEqualTo("\"hello world\"");
248 
249         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
250         verify(sMockProfiler).start(JSScriptEngineLogConstants.JAVA_EXECUTION_TIME);
251         verify(sSandboxInitWatch).stop();
252         verify(mIsolateCreateWatch).stop();
253         verify(mJavaExecutionWatch).stop();
254     }
255 
256     @Test
testCanRunAScriptWithNoArgs()257     public void testCanRunAScriptWithNoArgs() throws Exception {
258         assertThat(
259                         callJSEngine(
260                                 "function helloWorld() { return \"hello world\"; };",
261                                 ImmutableList.of(),
262                                 "helloWorld",
263                                 mDefaultIsolateSettings,
264                                 mNoOpRetryStrategy))
265                 .isEqualTo("\"hello world\"");
266     }
267 
268     @Test
testCanRunSimpleScriptWithOneArg()269     public void testCanRunSimpleScriptWithOneArg() throws Exception {
270         assertThat(
271                         callJSEngine(
272                                 "function hello(name) { return \"hello \" + name; };",
273                                 ImmutableList.of(stringArg("name", "Stefano")),
274                                 "hello",
275                                 mDefaultIsolateSettings,
276                                 mNoOpRetryStrategy))
277                 .isEqualTo("\"hello Stefano\"");
278     }
279 
280     @Test
testCanRunAScriptWithOneArg()281     public void testCanRunAScriptWithOneArg() throws Exception {
282         assertThat(
283                         callJSEngine(
284                                 "function helloPerson(personName) { return \"hello \" + personName;"
285                                         + " };",
286                                 ImmutableList.of(stringArg("name", "Stefano")),
287                                 "helloPerson",
288                                 mDefaultIsolateSettings,
289                                 mNoOpRetryStrategy))
290                 .isEqualTo("\"hello Stefano\"");
291     }
292 
293     @Test
testCanUseJSONArguments()294     public void testCanUseJSONArguments() throws Exception {
295         assertThat(
296                         callJSEngine(
297                                 "function helloPerson(person) {  return \"hello \" + person.name; "
298                                         + " };",
299                                 ImmutableList.of(
300                                         recordArg("jsonArg", stringArg("name", "Stefano"))),
301                                 "helloPerson",
302                                 mDefaultIsolateSettings,
303                                 mNoOpRetryStrategy))
304                 .isEqualTo("\"hello Stefano\"");
305     }
306 
307     @Test
testCanNotReferToScriptArguments()308     public void testCanNotReferToScriptArguments() {
309         ExecutionException e =
310                 assertThrows(
311                         ExecutionException.class,
312                         () ->
313                                 callJSEngine(
314                                         "function helloPerson(person) {  return \"hello \" +"
315                                                 + " personOuter.name;  };",
316                                         ImmutableList.of(
317                                                 recordArg(
318                                                         "personOuter",
319                                                         stringArg("name", "Stefano"))),
320                                         "helloPerson",
321                                         mDefaultIsolateSettings,
322                                         mNoOpRetryStrategy));
323 
324         assertThat(e).hasCauseThat().isInstanceOf(JSExecutionException.class);
325     }
326 
327     // During tests, look for logcat messages with tag "chromium" to check if any of your scripts
328     // have syntax errors. Those messages won't be available on prod builds (need to register
329     // a listener to WebChromeClient.onConsoleMessage to receive them if needed).
330     @Test
testWillReturnAStringWithContentNullEvaluatingScriptWithErrors()331     public void testWillReturnAStringWithContentNullEvaluatingScriptWithErrors() {
332         ExecutionException e =
333                 assertThrows(
334                         ExecutionException.class,
335                         () ->
336                                 callJSEngine(
337                                         "function test() { return \"hello world\"; }",
338                                         ImmutableList.of(),
339                                         "undefinedFunction",
340                                         mDefaultIsolateSettings,
341                                         mNoOpRetryStrategy));
342 
343         assertThat(e).hasCauseThat().isInstanceOf(JSExecutionException.class);
344     }
345 
346     @Test
testParallelCallsToTheScriptEngineDoNotInterfere()347     public void testParallelCallsToTheScriptEngineDoNotInterfere() throws Exception {
348         CountDownLatch resultsLatch = new CountDownLatch(2);
349 
350         final ImmutableList<JSScriptArgument> arguments =
351                 ImmutableList.of(recordArg("jsonArg", stringArg("name", "Stefano")));
352 
353         ListenableFuture<String> firstCallResult =
354                 callJSEngineAsync(
355                         "function helloPerson(person) {  return \"hello \" + person.name; " + " };",
356                         arguments,
357                         "helloPerson",
358                         resultsLatch,
359                         mDefaultIsolateSettings,
360                         mNoOpRetryStrategy);
361 
362         // The previous call reset the status, we can redefine the function and use the same
363         // argument
364         ListenableFuture<String> secondCallResult =
365                 callJSEngineAsync(
366                         "function helloPerson(person) {  return \"hello again \" + person.name; "
367                                 + " };",
368                         arguments,
369                         "helloPerson",
370                         resultsLatch,
371                         mDefaultIsolateSettings,
372                         mNoOpRetryStrategy);
373 
374         resultsLatch.await();
375 
376         assertThat(firstCallResult.get()).isEqualTo("\"hello Stefano\"");
377 
378         assertThat(secondCallResult.get()).isEqualTo("\"hello again Stefano\"");
379     }
380 
381     @Test
testCanHandleFailuresFromWebView()382     public void testCanHandleFailuresFromWebView() throws Exception {
383         Assume.assumeFalse(sJSScriptEngine.isLargeTransactionsSupported().get(1, TimeUnit.SECONDS));
384 
385         when(sMockProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
386                 .thenReturn(sSandboxInitWatch);
387         doNothing().when(sSandboxInitWatch).stop();
388 
389         // The binder can transfer at most 1MB, this is larger than needed since, once
390         // converted into a JS array initialization script will be way over the limits.
391         List<JSScriptNumericArgument<Integer>> tooBigForBinder =
392                 Arrays.stream(new int[1024 * 1024])
393                         .boxed()
394                         .map(value -> numericArg("_", value))
395                         .collect(Collectors.toList());
396         ExecutionException outerException =
397                 assertThrows(
398                         ExecutionException.class,
399                         () ->
400                                 callJSEngine(
401                                         "function helloBigArray(array) {\n"
402                                                 + " return array.length;\n"
403                                                 + "}",
404                                         ImmutableList.of(arrayArg("array", tooBigForBinder)),
405                                         "helloBigArray",
406                                         mDefaultIsolateSettings,
407                                         mNoOpRetryStrategy));
408 
409         assertThat(outerException).hasCauseThat().isInstanceOf(JSExecutionException.class);
410         // assert that we can recover from this exception
411         assertThat(
412                         callJSEngine(
413                                 "function test() { return \"hello world\"; }",
414                                 ImmutableList.of(),
415                                 "test",
416                                 mDefaultIsolateSettings,
417                                 new RetryStrategyImpl(1, mExecutorService)))
418                 .isEqualTo("\"hello world\"");
419     }
420 
421     @Test
testCanHandleLargeTransactionsToWebView()422     public void testCanHandleLargeTransactionsToWebView() throws Exception {
423         Assume.assumeTrue(sJSScriptEngine.isLargeTransactionsSupported().get(1, TimeUnit.SECONDS));
424         List<JSScriptNumericArgument<Integer>> tooBigForBinder =
425                 Arrays.stream(new int[1024 * 1024])
426                         .boxed()
427                         .map(value -> numericArg("_", value))
428                         .collect(Collectors.toList());
429 
430         String result =
431                 callJSEngine(
432                         "function helloBigArray(array) {\n" + " return array.length;\n" + "}",
433                         ImmutableList.of(arrayArg("array", tooBigForBinder)),
434                         "helloBigArray",
435                         mDefaultIsolateSettings,
436                         mNoOpRetryStrategy);
437         assertThat(Integer.parseInt(result)).isEqualTo(1024 * 1024);
438     }
439 
440     @Test
testCanCloseAndThenWorkWithSameInstance()441     public void testCanCloseAndThenWorkWithSameInstance() throws Exception {
442         when(sMockProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME))
443                 .thenReturn(sSandboxInitWatch);
444         doNothing().when(sSandboxInitWatch).stop();
445         assertThat(
446                         callJSEngine(
447                                 "function test() { return \"hello world\"; }",
448                                 ImmutableList.of(),
449                                 "test",
450                                 mDefaultIsolateSettings,
451                                 mNoOpRetryStrategy))
452                 .isEqualTo("\"hello world\"");
453 
454         sJSScriptEngine.shutdown().get(3, TimeUnit.SECONDS);
455 
456         assertThat(
457                         callJSEngine(
458                                 "function test() { return \"hello world\"; }",
459                                 ImmutableList.of(),
460                                 "test",
461                                 mDefaultIsolateSettings,
462                                 mNoOpRetryStrategy))
463                 .isEqualTo("\"hello world\"");
464 
465         // Engine is re-initialized
466         verify(sMockProfiler, atLeastOnce()).start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
467         verify(sSandboxInitWatch, atLeastOnce()).stop();
468     }
469 
470     @Test
testConnectionIsResetIfJSProcessIsTerminatedWithIllegalStateException()471     public void testConnectionIsResetIfJSProcessIsTerminatedWithIllegalStateException() {
472         when(mMockedSandbox.createIsolate())
473                 .thenThrow(
474                         new IllegalStateException(
475                                 "simulating a failure caused by JavaScriptSandbox being"
476                                         + " disconnected due to ISE"));
477 
478         ExecutionException executionException =
479                 callJSEngineAndAssertExecutionException(
480                         JSScriptEngine.createNewInstanceForTesting(
481                                 ApplicationProvider.getApplicationContext(),
482                                 mMockSandboxProvider,
483                                 sMockProfiler,
484                                 sLogger),
485                         mDefaultIsolateSettings);
486 
487         assertThat(executionException)
488                 .hasCauseThat()
489                 .isInstanceOf(JSScriptEngineConnectionException.class);
490         assertThat(executionException)
491                 .hasMessageThat()
492                 .contains(JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG);
493         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
494         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
495     }
496 
497     @Test
testConnectionIsResetIfCreateIsolateThrowsRuntimeException()498     public void testConnectionIsResetIfCreateIsolateThrowsRuntimeException() {
499         when(mMockedSandbox.createIsolate())
500                 .thenThrow(
501                         new RuntimeException(
502                                 "simulating a failure caused by JavaScriptSandbox being"
503                                         + " disconnected"));
504 
505         ExecutionException executionException =
506                 callJSEngineAndAssertExecutionException(
507                         JSScriptEngine.createNewInstanceForTesting(
508                                 ApplicationProvider.getApplicationContext(),
509                                 mMockSandboxProvider,
510                                 sMockProfiler,
511                                 sLogger),
512                         mDefaultIsolateSettings);
513 
514         assertThat(executionException)
515                 .hasCauseThat()
516                 .isInstanceOf(JSScriptEngineConnectionException.class);
517         assertThat(executionException)
518                 .hasMessageThat()
519                 .contains(JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG);
520         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
521         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
522     }
523 
524     @Test
testConnectionIsResetIfEvaluateFailsWithSandboxDeadException()525     public void testConnectionIsResetIfEvaluateFailsWithSandboxDeadException() {
526         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
527         when(mMockedIsolate.evaluateJavaScriptAsync(Mockito.anyString()))
528                 .thenReturn(Futures.immediateFailedFuture(new SandboxDeadException()));
529         when(mMockSandboxProvider.destroyIfCurrentInstance(mMockedSandbox))
530                 .thenReturn(Futures.immediateVoidFuture());
531 
532         ExecutionException executionException =
533                 callJSEngineAndAssertExecutionException(
534                         JSScriptEngine.createNewInstanceForTesting(
535                                 ApplicationProvider.getApplicationContext(),
536                                 mMockSandboxProvider,
537                                 sMockProfiler,
538                                 sLogger),
539                         mDefaultIsolateSettings);
540 
541         assertThat(executionException)
542                 .hasCauseThat()
543                 .isInstanceOf(JSScriptEngineConnectionException.class);
544         assertThat(executionException.getCause())
545                 .hasCauseThat()
546                 .isInstanceOf(SandboxDeadException.class);
547         assertThat(executionException).hasMessageThat().contains(JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG);
548         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
549     }
550 
551     @Test
testEvaluationIsRetriedIfEvaluateFailsWithSandboxDeadException()552     public void testEvaluationIsRetriedIfEvaluateFailsWithSandboxDeadException() throws Exception {
553         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
554         when(mMockedIsolate.evaluateJavaScriptAsync(Mockito.anyString()))
555                 .thenReturn(Futures.immediateFailedFuture(new SandboxDeadException()))
556                 .thenReturn(Futures.immediateFuture("{\"status\":200}"));
557         when(mMockSandboxProvider.destroyIfCurrentInstance(mMockedSandbox))
558                 .thenReturn(Futures.immediateVoidFuture());
559         RetryStrategy retryStrategy = new RetryStrategyImpl(1, mExecutorService);
560         assertEquals(
561                 callJSEngine(
562                         JSScriptEngine.createNewInstanceForTesting(
563                                 ApplicationProvider.getApplicationContext(),
564                                 mMockSandboxProvider,
565                                 sMockProfiler,
566                                 sLogger),
567                         "function test() { return \"hello world\"; }",
568                         ImmutableList.of(),
569                         "test",
570                         mDefaultIsolateSettings,
571                         retryStrategy),
572                 "{\"status\":200}");
573         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
574     }
575 
576     @Test
testConnectionIsResetIfEvaluateFailsWithMemoryLimitExceedException()577     public void testConnectionIsResetIfEvaluateFailsWithMemoryLimitExceedException() {
578         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
579         String expectedExceptionMessage = "Simulating Memory limit exceed exception from isolate";
580         when(mMockedIsolate.evaluateJavaScriptAsync(Mockito.anyString()))
581                 .thenReturn(
582                         Futures.immediateFailedFuture(
583                                 new MemoryLimitExceededException(expectedExceptionMessage)));
584         when(mMockSandboxProvider.destroyIfCurrentInstance(mMockedSandbox))
585                 .thenReturn(Futures.immediateVoidFuture());
586 
587         ExecutionException executionException =
588                 callJSEngineAndAssertExecutionException(
589                         JSScriptEngine.createNewInstanceForTesting(
590                                 ApplicationProvider.getApplicationContext(),
591                                 mMockSandboxProvider,
592                                 sMockProfiler,
593                                 sLogger),
594                         mDefaultIsolateSettings);
595 
596         assertThat(executionException).hasCauseThat().isInstanceOf(JSExecutionException.class);
597         assertThat(executionException.getCause())
598                 .hasCauseThat()
599                 .isInstanceOf(MemoryLimitExceededException.class);
600         assertThat(executionException).hasMessageThat().contains(expectedExceptionMessage);
601         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
602     }
603 
604     @Test
testConnectionIsNotResetIfEvaluateFailsWithAnyOtherException()605     public void testConnectionIsNotResetIfEvaluateFailsWithAnyOtherException() {
606         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
607         when(mMockedIsolate.evaluateJavaScriptAsync(Mockito.anyString()))
608                 .thenReturn(
609                         Futures.immediateFailedFuture(
610                                 new IllegalStateException("this is not SDE")));
611         when(mMockSandboxProvider.destroyIfCurrentInstance(mMockedSandbox))
612                 .thenReturn(Futures.immediateVoidFuture());
613 
614         ExecutionException executionException =
615                 callJSEngineAndAssertExecutionException(
616                         JSScriptEngine.createNewInstanceForTesting(
617                                 ApplicationProvider.getApplicationContext(),
618                                 mMockSandboxProvider,
619                                 sMockProfiler,
620                                 sLogger),
621                         mDefaultIsolateSettings);
622 
623         assertThat(executionException).hasCauseThat().isInstanceOf(JSExecutionException.class);
624         verify(mMockSandboxProvider, never()).destroyIfCurrentInstance(mMockedSandbox);
625     }
626 
627     @Test
testEnforceHeapMemorySizeFailureAtCreateIsolate()628     public void testEnforceHeapMemorySizeFailureAtCreateIsolate() {
629         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE))
630                 .thenReturn(true);
631         when(mMockedSandbox.createIsolate(Mockito.any(IsolateStartupParameters.class)))
632                 .thenThrow(
633                         new IllegalStateException(
634                                 "simulating a failure caused by JavaScriptSandbox not"
635                                         + " supporting max heap size"));
636         IsolateSettings enforcedHeapIsolateSettings =
637                 IsolateSettings.builder()
638                         .setEnforceMaxHeapSizeFeature(true)
639                         .setMaxHeapSizeBytes(1000)
640                         .setIsolateConsoleMessageInLogsEnabled(mDefaultIsolateConsoleMessageInLogs)
641                         .build();
642 
643         ExecutionException executionException =
644                 callJSEngineAndAssertExecutionException(
645                         JSScriptEngine.createNewInstanceForTesting(
646                                 ApplicationProvider.getApplicationContext(),
647                                 mMockSandboxProvider,
648                                 sMockProfiler,
649                                 sLogger),
650                         enforcedHeapIsolateSettings);
651 
652         assertThat(executionException)
653                 .hasCauseThat()
654                 .isInstanceOf(JSScriptEngineConnectionException.class);
655         assertThat(executionException)
656                 .hasMessageThat()
657                 .contains(JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG);
658         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
659         verify(mMockedSandbox)
660                 .isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
661     }
662 
663     @Test
testEnforceHeapMemorySizeUnsupportedBySandbox()664     public void testEnforceHeapMemorySizeUnsupportedBySandbox() {
665         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE))
666                 .thenReturn(false);
667         IsolateSettings enforcedHeapIsolateSettings =
668                 IsolateSettings.builder()
669                         .setEnforceMaxHeapSizeFeature(true)
670                         .setMaxHeapSizeBytes(1000)
671                         .setIsolateConsoleMessageInLogsEnabled(mDefaultIsolateConsoleMessageInLogs)
672                         .build();
673         ExecutionException executionException =
674                 callJSEngineAndAssertExecutionException(
675                         JSScriptEngine.createNewInstanceForTesting(
676                                 ApplicationProvider.getApplicationContext(),
677                                 mMockSandboxProvider,
678                                 sMockProfiler,
679                                 sLogger),
680                         enforcedHeapIsolateSettings);
681 
682         assertThat(executionException)
683                 .hasCauseThat()
684                 .isInstanceOf(JSScriptEngineConnectionException.class);
685         assertThat(executionException)
686                 .hasMessageThat()
687                 .contains(JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG);
688         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
689     }
690 
691     @Test
testLenientHeapMemorySize()692     public void testLenientHeapMemorySize() throws Exception {
693         // This exception though wired to be thrown will not be thrown
694         when(mMockedSandbox.createIsolate(Mockito.any(IsolateStartupParameters.class)))
695                 .thenThrow(
696                         new IllegalStateException(
697                                 "simulating a failure caused by JavaScriptSandbox not"
698                                         + " supporting max heap size"));
699         IsolateSettings lenientHeapIsolateSettings =
700                 IsolateSettings.forMaxHeapSizeEnforcementDisabled(
701                         mDefaultIsolateConsoleMessageInLogs);
702 
703         assertThat(
704                         callJSEngine(
705                                 "function test() { return \"hello world\"; }",
706                                 ImmutableList.of(),
707                                 "test",
708                                 lenientHeapIsolateSettings,
709                                 mNoOpRetryStrategy))
710                 .isEqualTo("\"hello world\"");
711     }
712 
713     @Test
testSuccessAtCreateIsolateUnboundedMaxHeapMemory()714     public void testSuccessAtCreateIsolateUnboundedMaxHeapMemory() throws Exception {
715         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE))
716                 .thenReturn(true);
717         IsolateSettings enforcedHeapIsolateSettings =
718                 IsolateSettings.builder()
719                         .setEnforceMaxHeapSizeFeature(true)
720                         .setMaxHeapSizeBytes(0)
721                         .setIsolateConsoleMessageInLogsEnabled(mDefaultIsolateConsoleMessageInLogs)
722                         .build();
723         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
724 
725         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
726                 .thenReturn(Futures.immediateFuture("\"hello world\""));
727 
728         assertThat(
729                         callJSEngine(
730                                 JSScriptEngine.createNewInstanceForTesting(
731                                         ApplicationProvider.getApplicationContext(),
732                                         mMockSandboxProvider,
733                                         sMockProfiler,
734                                         sLogger),
735                                 "function test() { return \"hello world\"; }",
736                                 ImmutableList.of(),
737                                 "test",
738                                 enforcedHeapIsolateSettings,
739                                 mNoOpRetryStrategy))
740                 .isEqualTo("\"hello world\"");
741 
742         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
743         verify(mMockedSandbox)
744                 .isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
745     }
746 
747     @Test
testSuccessAtCreateIsolateBoundedMaxHeapMemory()748     public void testSuccessAtCreateIsolateBoundedMaxHeapMemory() throws Exception {
749         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE))
750                 .thenReturn(true);
751         IsolateSettings enforcedHeapIsolateSettings =
752                 IsolateSettings.builder()
753                         .setEnforceMaxHeapSizeFeature(true)
754                         .setMaxHeapSizeBytes(1000)
755                         .setIsolateConsoleMessageInLogsEnabled(mDefaultIsolateConsoleMessageInLogs)
756                         .build();
757         when(mMockedSandbox.createIsolate(Mockito.any(IsolateStartupParameters.class)))
758                 .thenReturn(mMockedIsolate);
759 
760         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
761                 .thenReturn(Futures.immediateFuture("\"hello world\""));
762 
763         assertThat(
764                         callJSEngine(
765                                 JSScriptEngine.createNewInstanceForTesting(
766                                         ApplicationProvider.getApplicationContext(),
767                                         mMockSandboxProvider,
768                                         sMockProfiler,
769                                         sLogger),
770                                 "function test() { return \"hello world\"; }",
771                                 ImmutableList.of(),
772                                 "test",
773                                 enforcedHeapIsolateSettings,
774                                 mNoOpRetryStrategy))
775                 .isEqualTo("\"hello world\"");
776 
777         verify(sMockProfiler).start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
778         verify(mMockedSandbox)
779                 .isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
780     }
781 
782     @Test
testConsoleMessageCallbackSuccess()783     public void testConsoleMessageCallbackSuccess() throws Exception {
784         IsolateSettings isolateSettings = IsolateSettings.forMaxHeapSizeEnforcementDisabled(true);
785         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING))
786                 .thenReturn(true);
787         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
788         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
789                 .thenReturn(Futures.immediateFuture("\"hello world\""));
790 
791         callJSEngine(
792                 JSScriptEngine.createNewInstanceForTesting(
793                         ApplicationProvider.getApplicationContext(),
794                         mMockSandboxProvider,
795                         sMockProfiler,
796                         sLogger),
797                 "function test() { return \"hello world\"; }",
798                 ImmutableList.of(),
799                 "test",
800                 isolateSettings,
801                 mNoOpRetryStrategy);
802 
803         verify(mMockedSandbox).isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING);
804         verify(mMockedIsolate)
805                 .setConsoleCallback(
806                         any(ExecutorService.class), any(JavaScriptConsoleCallback.class));
807     }
808 
809     @Test
testConsoleMessageCallbackIsNotAddedWhenDisabled()810     public void testConsoleMessageCallbackIsNotAddedWhenDisabled() throws Exception {
811         IsolateSettings isolateSettings = IsolateSettings.forMaxHeapSizeEnforcementDisabled(false);
812         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
813         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
814                 .thenReturn(Futures.immediateFuture("\"hello world\""));
815 
816         callJSEngine(
817                 JSScriptEngine.createNewInstanceForTesting(
818                         ApplicationProvider.getApplicationContext(),
819                         mMockSandboxProvider,
820                         sMockProfiler,
821                         sLogger),
822                 "function test() { return \"hello world\"; }",
823                 ImmutableList.of(),
824                 "test",
825                 isolateSettings,
826                 mNoOpRetryStrategy);
827 
828         verify(mMockedSandbox, never())
829                 .isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING);
830         verify(mMockedIsolate, never())
831                 .setConsoleCallback(
832                         any(ExecutorService.class), any(JavaScriptConsoleCallback.class));
833     }
834 
835     @Test
testConsoleMessageCallbackIsNotSetIfFeatureNotAvailable()836     public void testConsoleMessageCallbackIsNotSetIfFeatureNotAvailable() throws Exception {
837         IsolateSettings isolateSettings = IsolateSettings.forMaxHeapSizeEnforcementDisabled(true);
838         when(mMockedSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING))
839                 .thenReturn(false);
840         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
841         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
842                 .thenReturn(Futures.immediateFuture("\"hello world\""));
843 
844         callJSEngine(
845                 JSScriptEngine.createNewInstanceForTesting(
846                         ApplicationProvider.getApplicationContext(),
847                         mMockSandboxProvider,
848                         sMockProfiler,
849                         sLogger),
850                 "function test() { return \"hello world\"; }",
851                 ImmutableList.of(),
852                 "test",
853                 isolateSettings,
854                 mNoOpRetryStrategy);
855 
856         verify(mMockedSandbox).isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING);
857         verify(mMockedIsolate, never())
858                 .setConsoleCallback(
859                         any(ExecutorService.class), any(JavaScriptConsoleCallback.class));
860     }
861 
862     // Troubles between google-java-format and checkstyle
863     // CHECKSTYLE:OFF IndentationCheck
864     @Test
testIsolateIsClosedWhenEvaluationCompletes()865     public void testIsolateIsClosedWhenEvaluationCompletes() throws Exception {
866         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
867         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
868                 .thenReturn(Futures.immediateFuture("hello world"));
869 
870         AtomicBoolean isolateHasBeenClosed = new AtomicBoolean(false);
871         CountDownLatch isolateIsClosedLatch = new CountDownLatch(1);
872         doAnswer(
873                         invocation -> {
874                             isolateHasBeenClosed.set(true);
875                             isolateIsClosedLatch.countDown();
876                             return null;
877                         })
878                 .when(mMockedIsolate)
879                 .close();
880 
881         callJSEngine(
882                 JSScriptEngine.createNewInstanceForTesting(
883                         ApplicationProvider.getApplicationContext(),
884                         mMockSandboxProvider,
885                         sMockProfiler,
886                         sLogger),
887                 "function test() { return \"hello world\"; }",
888                 ImmutableList.of(),
889                 "test",
890                 mDefaultIsolateSettings,
891                 mNoOpRetryStrategy);
892 
893         isolateIsClosedLatch.await(1, TimeUnit.SECONDS);
894         // Using Mockito.verify made the test unstable (mockito call registration was in a
895         // race condition with the verify call)
896         assertTrue(isolateHasBeenClosed.get());
897     }
898 
899     @Test
testIsolateIsClosedWhenEvaluationFails()900     public void testIsolateIsClosedWhenEvaluationFails() throws Exception {
901         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
902         when(mMockedIsolate.evaluateJavaScriptAsync(anyString()))
903                 .thenReturn(
904                         Futures.immediateFailedFuture(new RuntimeException("JS execution failed")));
905 
906         AtomicBoolean isolateHasBeenClosed = new AtomicBoolean(false);
907         CountDownLatch isolateIsClosedLatch = new CountDownLatch(1);
908         doAnswer(
909                         invocation -> {
910                             isolateHasBeenClosed.set(true);
911                             isolateIsClosedLatch.countDown();
912                             return null;
913                         })
914                 .when(mMockedIsolate)
915                 .close();
916 
917         assertThrows(
918                 ExecutionException.class,
919                 () ->
920                         callJSEngine(
921                                 JSScriptEngine.createNewInstanceForTesting(
922                                         ApplicationProvider.getApplicationContext(),
923                                         mMockSandboxProvider,
924                                         sMockProfiler,
925                                         sLogger),
926                                 "function test() { return \"hello world\"; }",
927                                 ImmutableList.of(),
928                                 "test",
929                                 mDefaultIsolateSettings,
930                                 mNoOpRetryStrategy));
931 
932         isolateIsClosedLatch.await(1, TimeUnit.SECONDS);
933         // Using Mockito.verify made the test unstable (mockito call registration was in a
934         // race condition with the verify call)
935         assertTrue(isolateHasBeenClosed.get());
936     }
937 
938     @Test
testIsolateIsClosedWhenEvaluationIsCancelled()939     public void testIsolateIsClosedWhenEvaluationIsCancelled() throws Exception {
940         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
941 
942         CountDownLatch jsEvaluationStartedLatch = new CountDownLatch(1);
943         CountDownLatch stallJsEvaluationLatch = new CountDownLatch(1);
944         ListeningExecutorService callbackExecutor =
945                 MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
946         doAnswer(
947                         invocation -> {
948                             jsEvaluationStartedLatch.countDown();
949                             sLogger.i("JS execution started");
950                             return callbackExecutor.submit(
951                                     () -> {
952                                         try {
953                                             stallJsEvaluationLatch.await();
954                                         } catch (InterruptedException ignored) {
955                                             Thread.currentThread().interrupt();
956                                         }
957                                         sLogger.i("JS execution completed,");
958                                         return "hello world";
959                                     });
960                         })
961                 .when(mMockedIsolate)
962                 .evaluateJavaScriptAsync(anyString());
963 
964         JSScriptEngine engine =
965                 JSScriptEngine.createNewInstanceForTesting(
966                         sContext, mMockSandboxProvider, sMockProfiler, sLogger);
967         ListenableFuture<String> jsExecutionFuture =
968                 engine.evaluate(
969                         "function test() { return \"hello world\"; }",
970                         ImmutableList.of(),
971                         "test",
972                         mDefaultIsolateSettings,
973                         mNoOpRetryStrategy);
974 
975         // Cancelling only after the processing started and the sandbox has been created
976         jsEvaluationStartedLatch.await(1, TimeUnit.SECONDS);
977         // Explicitly verifying that isolate was created as latch could have just counted down
978         verify(mMockedSandbox).createIsolate();
979         assertTrue(
980                 "Execution for the future should have been still ongoing when cancelled",
981                 jsExecutionFuture.cancel(true));
982         verify(mMockedIsolate, timeout(2000).atLeast(1)).close();
983     }
984 
985     @Test
testIsolateIsClosedWhenEvaluationTimesOut()986     public void testIsolateIsClosedWhenEvaluationTimesOut() throws Exception {
987         when(mMockedSandbox.createIsolate()).thenReturn(mMockedIsolate);
988         CountDownLatch jsEvaluationStartedLatch = new CountDownLatch(1);
989         CountDownLatch stallJsEvaluationLatch = new CountDownLatch(1);
990         ListeningExecutorService callbackExecutor =
991                 MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
992         doAnswer(
993                         invocation -> {
994                             jsEvaluationStartedLatch.countDown();
995                             sLogger.i("JS execution started");
996                             return callbackExecutor.submit(
997                                     () -> {
998                                         try {
999                                             stallJsEvaluationLatch.await();
1000                                         } catch (InterruptedException ignored) {
1001                                             Thread.currentThread().interrupt();
1002                                         }
1003                                         sLogger.i("JS execution completed");
1004                                         return "hello world";
1005                                     });
1006                         })
1007                 .when(mMockedIsolate)
1008                 .evaluateJavaScriptAsync(anyString());
1009 
1010         JSScriptEngine engine =
1011                 JSScriptEngine.createNewInstanceForTesting(
1012                         ApplicationProvider.getApplicationContext(),
1013                         mMockSandboxProvider,
1014                         sMockProfiler,
1015                         sLogger);
1016         ExecutionException timeoutException =
1017                 assertThrows(
1018                         ExecutionException.class,
1019                         () ->
1020                                 FluentFuture.from(
1021                                                 engine.evaluate(
1022                                                         "function test() { return \"hello world\";"
1023                                                                 + " }",
1024                                                         ImmutableList.of(),
1025                                                         "test",
1026                                                         mDefaultIsolateSettings,
1027                                                         mNoOpRetryStrategy))
1028                                         .withTimeout(
1029                                                 500,
1030                                                 TimeUnit.MILLISECONDS,
1031                                                 new ScheduledThreadPoolExecutor(1))
1032                                         .get());
1033 
1034         jsEvaluationStartedLatch.await(1, TimeUnit.SECONDS);
1035         // Explicitly verifying that isolate was created as latch could have just counted down
1036         verify(mMockedSandbox).createIsolate();
1037         // Verifying close was invoked
1038         verify(mMockedIsolate, timeout(2000).atLeast(1)).close();
1039         assertThat(timeoutException).hasCauseThat().isInstanceOf(TimeoutException.class);
1040     }
1041     // CHECKSTYLE:ON IndentationCheck
1042 
1043     @Test
testThrowsExceptionAndRecreateSandboxIfIsolateCreationFails()1044     public void testThrowsExceptionAndRecreateSandboxIfIsolateCreationFails() throws Exception {
1045         doThrow(new RuntimeException("Simulating isolate creation failure"))
1046                 .when(mMockedSandbox)
1047                 .createIsolate();
1048 
1049         JSScriptEngine engine =
1050                 JSScriptEngine.createNewInstanceForTesting(
1051                         ApplicationProvider.getApplicationContext(),
1052                         mMockSandboxProvider,
1053                         sMockProfiler,
1054                         sLogger);
1055 
1056         assertThrows(
1057                 ExecutionException.class,
1058                 () ->
1059                         callJSEngine(
1060                                 engine,
1061                                 "function test() { return \"hello world\";" + " }",
1062                                 ImmutableList.of(),
1063                                 "test",
1064                                 mDefaultIsolateSettings,
1065                                 mNoOpRetryStrategy));
1066         verify(mMockSandboxProvider).destroyIfCurrentInstance(mMockedSandbox);
1067     }
1068 
1069     @Test
testCanUseWasmModuleInScript()1070     public void testCanUseWasmModuleInScript() throws Exception {
1071         assumeTrue(sJSScriptEngine.isWasmSupported().get(4, TimeUnit.SECONDS));
1072 
1073         String jsUsingWasmModule =
1074                 "\"use strict\";\n"
1075                         + "\n"
1076                         + "function callWasm(input, wasmModule) {\n"
1077                         + "  const instance = new WebAssembly.Instance(wasmModule);\n"
1078                         + "\n"
1079                         + "  return instance.exports._fact(input);\n"
1080                         + "\n"
1081                         + "}";
1082 
1083         String result =
1084                 callJSEngine(
1085                         jsUsingWasmModule,
1086                         readBinaryAsset(WASM_MODULE),
1087                         ImmutableList.of(numericArg("input", 3)),
1088                         "callWasm",
1089                         mDefaultIsolateSettings,
1090                         mNoOpRetryStrategy);
1091 
1092         assertThat(result).isEqualTo("6");
1093     }
1094 
1095     @Test
testCanNotUseWasmModuleInScriptIfWebViewDoesNotSupportWasm()1096     public void testCanNotUseWasmModuleInScriptIfWebViewDoesNotSupportWasm() throws Exception {
1097         assumeFalse(sJSScriptEngine.isWasmSupported().get(4, TimeUnit.SECONDS));
1098 
1099         String jsUsingWasmModule =
1100                 "\"use strict\";\n"
1101                         + "\n"
1102                         + "function callWasm(input, wasmModule) {\n"
1103                         + "  const instance = new WebAssembly.Instance(wasmModule);\n"
1104                         + "\n"
1105                         + "  return instance.exports._fact(input);\n"
1106                         + "\n"
1107                         + "}";
1108 
1109         ExecutionException outer =
1110                 assertThrows(
1111                         ExecutionException.class,
1112                         () ->
1113                                 callJSEngine(
1114                                         jsUsingWasmModule,
1115                                         readBinaryAsset(WASM_MODULE),
1116                                         ImmutableList.of(numericArg("input", 3)),
1117                                         "callWasm",
1118                                         mDefaultIsolateSettings,
1119                                         mNoOpRetryStrategy));
1120 
1121         assertThat(outer).hasCauseThat().isInstanceOf(IllegalStateException.class);
1122     }
1123 
callJSEngineAndAssertExecutionException( JSScriptEngine engine, IsolateSettings isolateSettings)1124     private ExecutionException callJSEngineAndAssertExecutionException(
1125             JSScriptEngine engine, IsolateSettings isolateSettings) {
1126         return assertThrows(
1127                 ExecutionException.class,
1128                 () ->
1129                         callJSEngine(
1130                                 engine,
1131                                 "function test() { return \"hello world\"; }",
1132                                 ImmutableList.of(),
1133                                 "test",
1134                                 isolateSettings,
1135                                 mNoOpRetryStrategy));
1136     }
1137 
callJSEngine( @onNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1138     private String callJSEngine(
1139             @NonNull String jsScript,
1140             @NonNull List<JSScriptArgument> args,
1141             @NonNull String functionName,
1142             @NonNull IsolateSettings isolateSettings,
1143             @NonNull RetryStrategy retryStrategy)
1144             throws Exception {
1145         return callJSEngine(
1146                 sJSScriptEngine, jsScript, args, functionName, isolateSettings, retryStrategy);
1147     }
1148 
callJSEngine( @onNull String jsScript, @NonNull byte[] wasmBytes, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1149     private String callJSEngine(
1150             @NonNull String jsScript,
1151             @NonNull byte[] wasmBytes,
1152             @NonNull List<JSScriptArgument> args,
1153             @NonNull String functionName,
1154             @NonNull IsolateSettings isolateSettings,
1155             @NonNull RetryStrategy retryStrategy)
1156             throws Exception {
1157         return callJSEngine(
1158                 sJSScriptEngine,
1159                 jsScript,
1160                 wasmBytes,
1161                 args,
1162                 functionName,
1163                 isolateSettings,
1164                 retryStrategy);
1165     }
1166 
callJSEngine( @onNull JSScriptEngine jsScriptEngine, @NonNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1167     private String callJSEngine(
1168             @NonNull JSScriptEngine jsScriptEngine,
1169             @NonNull String jsScript,
1170             @NonNull List<JSScriptArgument> args,
1171             @NonNull String functionName,
1172             @NonNull IsolateSettings isolateSettings,
1173             @NonNull RetryStrategy retryStrategy)
1174             throws Exception {
1175         CountDownLatch resultLatch = new CountDownLatch(1);
1176         ListenableFuture<String> futureResult =
1177                 callJSEngineAsync(
1178                         jsScriptEngine,
1179                         jsScript,
1180                         args,
1181                         functionName,
1182                         resultLatch,
1183                         isolateSettings,
1184                         retryStrategy);
1185         resultLatch.await();
1186         return futureResult.get();
1187     }
1188 
callJSEngine( @onNull JSScriptEngine jsScriptEngine, @NonNull String jsScript, @NonNull byte[] wasmBytes, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1189     private String callJSEngine(
1190             @NonNull JSScriptEngine jsScriptEngine,
1191             @NonNull String jsScript,
1192             @NonNull byte[] wasmBytes,
1193             @NonNull List<JSScriptArgument> args,
1194             @NonNull String functionName,
1195             @NonNull IsolateSettings isolateSettings,
1196             @NonNull RetryStrategy retryStrategy)
1197             throws Exception {
1198         CountDownLatch resultLatch = new CountDownLatch(1);
1199         ListenableFuture<String> futureResult =
1200                 callJSEngineAsync(
1201                         jsScriptEngine,
1202                         jsScript,
1203                         wasmBytes,
1204                         args,
1205                         functionName,
1206                         resultLatch,
1207                         isolateSettings,
1208                         retryStrategy);
1209         resultLatch.await();
1210         return futureResult.get();
1211     }
1212 
callJSEngineAsync( @onNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull CountDownLatch resultLatch, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1213     private ListenableFuture<String> callJSEngineAsync(
1214             @NonNull String jsScript,
1215             @NonNull List<JSScriptArgument> args,
1216             @NonNull String functionName,
1217             @NonNull CountDownLatch resultLatch,
1218             @NonNull IsolateSettings isolateSettings,
1219             @NonNull RetryStrategy retryStrategy) {
1220         return callJSEngineAsync(
1221                 sJSScriptEngine,
1222                 jsScript,
1223                 args,
1224                 functionName,
1225                 resultLatch,
1226                 isolateSettings,
1227                 retryStrategy);
1228     }
1229 
callJSEngineAsync( @onNull JSScriptEngine engine, @NonNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull CountDownLatch resultLatch, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1230     private ListenableFuture<String> callJSEngineAsync(
1231             @NonNull JSScriptEngine engine,
1232             @NonNull String jsScript,
1233             @NonNull List<JSScriptArgument> args,
1234             @NonNull String functionName,
1235             @NonNull CountDownLatch resultLatch,
1236             @NonNull IsolateSettings isolateSettings,
1237             @NonNull RetryStrategy retryStrategy) {
1238         Objects.requireNonNull(engine);
1239         Objects.requireNonNull(resultLatch);
1240         sLogger.v("Calling JavaScriptSandbox");
1241         ListenableFuture<String> result =
1242                 engine.evaluate(jsScript, args, functionName, isolateSettings, retryStrategy);
1243         result.addListener(resultLatch::countDown, mExecutorService);
1244         return result;
1245     }
1246 
callJSEngineAsync( @onNull JSScriptEngine engine, @NonNull String jsScript, @NonNull byte[] wasmBytes, @NonNull List<JSScriptArgument> args, @NonNull String functionName, @NonNull CountDownLatch resultLatch, @NonNull IsolateSettings isolateSettings, @NonNull RetryStrategy retryStrategy)1247     private ListenableFuture<String> callJSEngineAsync(
1248             @NonNull JSScriptEngine engine,
1249             @NonNull String jsScript,
1250             @NonNull byte[] wasmBytes,
1251             @NonNull List<JSScriptArgument> args,
1252             @NonNull String functionName,
1253             @NonNull CountDownLatch resultLatch,
1254             @NonNull IsolateSettings isolateSettings,
1255             @NonNull RetryStrategy retryStrategy) {
1256         Objects.requireNonNull(engine);
1257         Objects.requireNonNull(resultLatch);
1258         sLogger.v("Calling JavaScriptSandbox");
1259         ListenableFuture<String> result =
1260                 engine.evaluate(
1261                         jsScript, wasmBytes, args, functionName, isolateSettings, retryStrategy);
1262         result.addListener(resultLatch::countDown, mExecutorService);
1263         return result;
1264     }
1265 
readBinaryAsset(@onNull String assetName)1266     private byte[] readBinaryAsset(@NonNull String assetName) throws IOException {
1267         InputStream inputStream = sContext.getAssets().open(assetName);
1268         return SdkLevel.isAtLeastT()
1269                 ? inputStream.readAllBytes()
1270                 : ByteStreams.toByteArray(inputStream);
1271     }
1272 }
1273