| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package androidx.javascriptengine; |
| |
| import android.content.Context; |
| |
| import androidx.test.core.app.ApplicationProvider; |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| import androidx.test.filters.MediumTest; |
| |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import org.junit.Assert; |
| import org.junit.Assume; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.util.Vector; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Instrumentation test for JavaScriptSandbox. */ |
| @RunWith(AndroidJUnit4.class) |
| public class WebViewJavaScriptSandboxTest { |
| // This value is somewhat arbitrary. It might need bumping if V8 snapshots become significantly |
| // larger in future. However, we don't want it too large as that will make the tests slower and |
| // require more memory. Although this is a long, it must not be greater than Integer.MAX_VALUE |
| // and should be much smaller (for the purposes of testing). |
| private static final long REASONABLE_HEAP_SIZE = 100 * 1024 * 1024; |
| |
| @Before |
| public void setUp() throws Throwable { |
| Assume.assumeTrue(JavaScriptSandbox.isSupported()); |
| } |
| |
| @Test |
| @MediumTest |
| public void testSimpleJsEvaluation() throws Throwable { |
| final String code = "\"PASS\""; |
| final String expected = "PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testClosingOneIsolate() throws Throwable { |
| final String code = "'PASS'"; |
| final String expected = "PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate()) { |
| JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate(); |
| jsIsolate2.close(); |
| |
| ListenableFuture<String> resultFuture = jsIsolate1.evaluateJavaScriptAsync(code); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testEvaluationInTwoIsolates() throws Throwable { |
| final String code1 = "this.x = 'PASS';\n"; |
| final String expected1 = "PASS"; |
| final String code2 = "this.x = 'SUPER_PASS';\n"; |
| final String expected2 = "SUPER_PASS"; |
| |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(); |
| JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1); |
| String result1 = resultFuture1.get(5, TimeUnit.SECONDS); |
| ListenableFuture<String> resultFuture2 = jsIsolate2.evaluateJavaScriptAsync(code2); |
| String result2 = resultFuture2.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected1, result1); |
| Assert.assertEquals(expected2, result2); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testTwoIsolatesDoNotShareEnvironment() throws Throwable { |
| final String code1 = "this.y = 'PASS';\n"; |
| final String expected1 = "PASS"; |
| final String code2 = "this.y = this.y + ' PASS';\n"; |
| final String expected2 = "undefined PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(); |
| JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1); |
| String result1 = resultFuture1.get(5, TimeUnit.SECONDS); |
| ListenableFuture<String> resultFuture2 = jsIsolate2.evaluateJavaScriptAsync(code2); |
| String result2 = resultFuture2.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected1, result1); |
| Assert.assertEquals(expected2, result2); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testTwoExecutionsShareEnvironment() throws Throwable { |
| final String code1 = "this.z = 'PASS';\n"; |
| final String expected1 = "PASS"; |
| final String code2 = "this.z = this.z + ' PASS';\n"; |
| final String expected2 = "PASS PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1); |
| String result1 = resultFuture1.get(5, TimeUnit.SECONDS); |
| ListenableFuture<String> resultFuture2 = jsIsolate1.evaluateJavaScriptAsync(code2); |
| String result2 = resultFuture2.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected1, result1); |
| Assert.assertEquals(expected2, result2); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testJsEvaluationError() throws Throwable { |
| final String code = "throw new WebAssembly.LinkError('RandomLinkError');"; |
| final String contains = "RandomLinkError"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| boolean isOfCorrectType = false; |
| String error = ""; |
| try { |
| resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| isOfCorrectType = e.getCause().getClass().equals(EvaluationFailedException.class); |
| error = e.getCause().getMessage(); |
| } |
| |
| Assert.assertTrue(isOfCorrectType); |
| Assert.assertTrue(error.contains(contains)); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testInfiniteLoop() throws Throwable { |
| final String code = "while(true){}"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_TERMINATION)); |
| |
| ListenableFuture<String> resultFuture; |
| try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| } |
| |
| try { |
| resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof IsolateTerminatedException)) { |
| throw e; |
| } |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testMultipleInfiniteLoops() throws Throwable { |
| final String code = "while(true){}"; |
| final int num_of_evaluations = 10; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_TERMINATION)); |
| |
| Vector<ListenableFuture<String>> resultFutures = new Vector<ListenableFuture<String>>(); |
| try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| for (int i = 0; i < num_of_evaluations; i++) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| resultFutures.add(resultFuture); |
| } |
| } |
| |
| for (int i = 0; i < num_of_evaluations; i++) { |
| try { |
| resultFutures.elementAt(i).get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof IsolateTerminatedException)) { |
| throw e; |
| } |
| } |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testSimpleArrayBuffer() throws Throwable { |
| final String provideString = "Hello World"; |
| final byte[] bytes = provideString.getBytes(StandardCharsets.US_ASCII); |
| final String code = "" |
| + "function ab2str(buf) {" |
| + " return String.fromCharCode.apply(null, new Uint8Array(buf));" |
| + "}" |
| + "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((value) => {" |
| + " return ab2str(value);" |
| + "});"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)); |
| |
| boolean provideNamedDataReturn = jsIsolate.provideNamedData("id-1", bytes); |
| Assert.assertTrue(provideNamedDataReturn); |
| ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture1.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(provideString, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testArrayBufferWasmCompilation() throws Throwable { |
| final String success = "success"; |
| // The bytes of a minimal WebAssembly module, courtesy of v8/test/cctest/test-api-wasm.cc |
| final byte[] bytes = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}; |
| final String code = "" |
| + "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((value) => {" |
| + " return WebAssembly.compile(value).then((module) => {" |
| + " return \"success\";" |
| + " });" |
| + "});"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)); |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_WASM_COMPILATION)); |
| |
| boolean provideNamedDataReturn = jsIsolate.provideNamedData("id-1", bytes); |
| Assert.assertTrue(provideNamedDataReturn); |
| ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture1.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(success, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testPromiseReturn() throws Throwable { |
| final String code = "Promise.resolve(\"PASS\")"; |
| final String expected = "PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testPromiseReturnLaterResolve() throws Throwable { |
| final String code1 = "var promiseResolve, promiseReject;" |
| + "new Promise(function(resolve, reject){" |
| + " promiseResolve = resolve;" |
| + " promiseReject = reject;" |
| + "});"; |
| final String code2 = "promiseResolve(\"PASS\");"; |
| final String expected = "PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| |
| ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code1); |
| ListenableFuture<String> resultFuture2 = jsIsolate.evaluateJavaScriptAsync(code2); |
| String result = resultFuture1.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testNestedConsumeNamedDataAsArrayBuffer() throws Throwable { |
| final String success = "success"; |
| // The bytes of a minimal WebAssembly module, courtesy of v8/test/cctest/test-api-wasm.cc |
| final byte[] bytes = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}; |
| final String code = "" |
| + "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((value) => {" |
| + " return android.consumeNamedDataAsArrayBuffer(\"id-2\").then((value) => {" |
| + " return android.consumeNamedDataAsArrayBuffer(\"id-3\").then((value) => {" |
| + " return android.consumeNamedDataAsArrayBuffer(\"id-4\").then((value) => {" |
| + " return android.consumeNamedDataAsArrayBuffer(\"id-5\").then((value) => {" |
| + " return \"success\";" |
| + " }, (error) => {" |
| + " return error.message;" |
| + " });" |
| + " });" |
| + " });" |
| + " });" |
| + "});"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)); |
| |
| jsIsolate.provideNamedData("id-1", bytes); |
| jsIsolate.provideNamedData("id-2", bytes); |
| jsIsolate.provideNamedData("id-3", bytes); |
| jsIsolate.provideNamedData("id-4", bytes); |
| jsIsolate.provideNamedData("id-5", bytes); |
| Thread.sleep(1000); |
| ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture1.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(success, result); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testPromiseEvaluationThrow() throws Throwable { |
| final String provideString = "Hello World"; |
| final byte[] bytes = provideString.getBytes(StandardCharsets.US_ASCII); |
| final String code = "" |
| + "android.consumeNamedDataAsArrayBuffer(\"id-1\").catch((error) => {" |
| + " throw new WebAssembly.LinkError('RandomLinkError');" |
| + "});"; |
| final String contains = "RandomLinkError"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)); |
| |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| try { |
| resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof EvaluationFailedException)) { |
| throw e; |
| } |
| Assert.assertTrue(e.getCause().getMessage().contains(contains)); |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testEvaluationThrowsWhenSandboxClosed() throws Throwable { |
| final String code = "while(true){}"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code); |
| jsSandbox.close(); |
| // Check already running evaluation gets SandboxDeadException |
| try { |
| resultFuture1.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof SandboxDeadException)) { |
| throw e; |
| } |
| } |
| // Check post-close evaluation gets SandboxDeadException |
| ListenableFuture<String> resultFuture2 = jsIsolate.evaluateJavaScriptAsync(code); |
| try { |
| resultFuture2.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof SandboxDeadException)) { |
| throw e; |
| } |
| } |
| // Check that closing an isolate then causes the IllegalStateException to be |
| // thrown instead. |
| jsIsolate.close(); |
| try { |
| ListenableFuture<String> postCloseResultFuture = |
| jsIsolate.evaluateJavaScriptAsync(code); |
| Assert.fail("Should have thrown."); |
| } catch (IllegalStateException e) { |
| // Expected |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testMultipleSandboxesCannotCoexist() throws Throwable { |
| Context context = ApplicationProvider.getApplicationContext(); |
| final String contains = "already bound"; |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox1 = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) { |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture2 = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try { |
| try (JavaScriptSandbox jsSandbox2 = jsSandboxFuture2.get(5, TimeUnit.SECONDS)) { |
| Assert.fail("Should have thrown."); |
| } |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof RuntimeException)) { |
| throw e; |
| } |
| Assert.assertTrue(e.getCause().getMessage().contains(contains)); |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testSandboxCanBeCreatedAfterClosed() throws Throwable { |
| final String code = "\"PASS\""; |
| final String expected = "PASS"; |
| final int num_of_startups = 2; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| for (int i = 0; i < num_of_startups; i++) { |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| |
| Assert.assertEquals(expected, result); |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testHeapSizeAdjustment() throws Throwable { |
| final String code = "\"PASS\""; |
| final String expected = "PASS"; |
| final long[] heapSizes = { |
| 0, |
| REASONABLE_HEAP_SIZE, |
| REASONABLE_HEAP_SIZE - 1, |
| REASONABLE_HEAP_SIZE + 1, |
| REASONABLE_HEAP_SIZE + 4095, |
| REASONABLE_HEAP_SIZE + 4096, |
| REASONABLE_HEAP_SIZE + 65535, |
| REASONABLE_HEAP_SIZE + 65536, |
| 1L << 50, |
| }; |
| Context context = ApplicationProvider.getApplicationContext(); |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) { |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)); |
| for (long heapSize : heapSizes) { |
| IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters(); |
| isolateStartupParameters.setMaxHeapSizeBytes(heapSize); |
| try (JavaScriptIsolate jsIsolate = |
| jsSandbox.createIsolate(isolateStartupParameters)) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(expected, result); |
| } catch (Throwable e) { |
| throw new AssertionError( |
| "Failed to evaluate JavaScript using max heap size setting " + heapSize, |
| e); |
| } |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testHeapSizeEnforced() throws Throwable { |
| final long maxHeapSize = REASONABLE_HEAP_SIZE; |
| // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note |
| // that we're allocating an array of elements - not bytes. Filling will ensure that the |
| // array is not sparsely allocated. |
| final String oomingCode = "" |
| + "const array = Array(" + maxHeapSize + ").fill(Math.random(), 0);"; |
| final String stableCode = "'PASS'"; |
| final String stableExpected = "PASS"; |
| final String unresolvedCode = "new Promise((resolve, reject) => {/* never resolve */})"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) { |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)); |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters(); |
| isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize); |
| try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters); |
| JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) { |
| ListenableFuture<String> earlyUnresolvedResultFuture = |
| jsIsolate1.evaluateJavaScriptAsync(unresolvedCode); |
| ListenableFuture<String> earlyResultFuture = |
| jsIsolate1.evaluateJavaScriptAsync(stableCode); |
| ListenableFuture<String> oomResultFuture = |
| jsIsolate1.evaluateJavaScriptAsync(oomingCode); |
| |
| // Wait for jsIsolate2 to fully initialize before using jsIsolate1. |
| jsIsolate2.evaluateJavaScriptAsync(stableCode).get(5, TimeUnit.SECONDS); |
| |
| // Check that the heap limit is enforced and that it reports this was the evaluation |
| // that exceeded the limit. |
| try { |
| oomResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof MemoryLimitExceededException)) { |
| throw e; |
| } |
| } |
| |
| // Check that the previously submitted (but unresolved) promise evaluation reports a |
| // crash |
| try { |
| earlyUnresolvedResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof IsolateTerminatedException)) { |
| throw e; |
| } |
| } |
| |
| // Check that the previously submitted evaluation which completed before the memory |
| // limit was exceeded, but for which we haven't yet gotten the result, returns its |
| // result just fine. |
| String result = earlyResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(stableExpected, result); |
| |
| // Check that a totally new evaluation reports a crash |
| ListenableFuture<String> lateResultFuture = |
| jsIsolate1.evaluateJavaScriptAsync(stableCode); |
| try { |
| lateResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof IsolateTerminatedException)) { |
| throw e; |
| } |
| } |
| |
| // Check that other pre-existing isolates can still be used. |
| ListenableFuture<String> otherIsolateResultFuture = |
| jsIsolate2.evaluateJavaScriptAsync(stableCode); |
| String otherIsolateResult = otherIsolateResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(stableExpected, otherIsolateResult); |
| } |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testIsolateCreationAfterCrash() throws Throwable { |
| final long maxHeapSize = REASONABLE_HEAP_SIZE; |
| // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note |
| // that we're allocating an array of elements - not bytes. Filling will ensure that the |
| // array is not sparsely allocated. |
| final String oomingCode = "" |
| + "const array = Array(" + maxHeapSize + ").fill(Math.random(), 0);"; |
| final String stableCode = "'PASS'"; |
| final String stableExpected = "PASS"; |
| Context context = ApplicationProvider.getApplicationContext(); |
| |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) { |
| Assume.assumeTrue(jsSandbox.isFeatureSupported( |
| JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)); |
| Assume.assumeTrue( |
| jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)); |
| IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters(); |
| isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize); |
| try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters)) { |
| ListenableFuture<String> oomResultFuture = |
| jsIsolate1.evaluateJavaScriptAsync(oomingCode); |
| |
| // Check that the heap limit is enforced and that it reports this was the evaluation |
| // that exceeded the limit. |
| try { |
| oomResultFuture.get(5, TimeUnit.SECONDS); |
| Assert.fail("Should have thrown."); |
| } catch (ExecutionException e) { |
| if (!(e.getCause() instanceof MemoryLimitExceededException)) { |
| throw e; |
| } |
| } |
| |
| // Check that other isolates can still be created and used (without closing |
| // jsIsolate1). |
| try (JavaScriptIsolate jsIsolate2 = |
| jsSandbox.createIsolate(isolateStartupParameters)) { |
| ListenableFuture<String> resultFuture = |
| jsIsolate2.evaluateJavaScriptAsync(stableCode); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(stableExpected, result); |
| } |
| } |
| |
| // Check that other isolates can still be created and used (after closing jsIsolate1). |
| try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) { |
| ListenableFuture<String> resultFuture = |
| jsIsolate.evaluateJavaScriptAsync(stableCode); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(stableExpected, result); |
| } |
| } |
| |
| // Check that the old sandbox with the "crashed" isolate can be torn down and that a new |
| // sandbox and isolate can be spun up. |
| ListenableFuture<JavaScriptSandbox> jsSandboxFuture2 = |
| JavaScriptSandbox.createConnectedInstanceAsync(context); |
| try (JavaScriptSandbox jsSandbox = jsSandboxFuture2.get(5, TimeUnit.SECONDS); |
| JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) { |
| ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(stableCode); |
| String result = resultFuture.get(5, TimeUnit.SECONDS); |
| Assert.assertEquals(stableExpected, result); |
| } |
| } |
| } |