package com.oracle.truffle.js.test.interop;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.junit.BeforeClass;
import org.junit.Test;
import com.oracle.truffle.js.runtime.GraalJSException;
import com.oracle.truffle.js.runtime.JSException;
import com.oracle.truffle.js.test.JSTest;
public class ExceptionInteropTest {
public static class ToBePassedToJS {
private final Engine engine;
private final HostAccess hostAccess;
private Exception exception;
private String exceptionName;
private String exceptionMessage;
private Map<?, ?> map;
private GraalJSException.JSStackTraceElement[] jsStackTrace;
public ToBePassedToJS(Engine engine, HostAccess hostAccess) {
this.engine = engine;
this.hostAccess = hostAccess;
}
@HostAccess.Export
public void methodThatThrowsJSException() throws IOException {
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(engine).build()) {
context.eval(Source.newBuilder(ID, "\nisNotDefined.doesNotMakeSense = 1;\n", "nestedisnotdefined.js").build());
fail("PolyglotException not thrown");
}
}
@HostAccess.Export
public void methodThatThrowsException() {
this.jsStackTrace = JSException.getJSStackTrace(null);
throw new UnsupportedOperationException("This operation is not supported!");
}
@HostAccess.Export
public void methodThatThrowsNestedJavaException() throws IOException {
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(engine).build()) {
Value bindings = context.getBindings(ID);
bindings.putMember("objectFromJava", this);
context.eval(Source.newBuilder(ID, "\nobjectFromJava.methodThatThrowsException();\n",
"nestedhostexception.js").build());
}
}
@HostAccess.Export
public void methodWithExceptionArgument(Exception argException, String name, String message) {
this.exception = argException;
this.exceptionName = name;
this.exceptionMessage = message;
}
@HostAccess.Export
public void methodWithMapArgument(Map<?, ?> argMap) {
this.map = argMap;
}
}
private static int EXCEPTION_LINE_NUMBER;
@BeforeClass
public static void initializeExceptionLineNumber() {
HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
int lineNumber = -1;
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().engine(graalEngine).allowHostAccess(hostAccess).build()) {
context.enter();
context.initialize(ID);
ToBePassedToJS toBePassedToJS = new ToBePassedToJS(null, null);
try {
toBePassedToJS.methodThatThrowsException();
} catch (UnsupportedOperationException e) {
lineNumber = e.getStackTrace()[0].getLineNumber();
}
context.leave();
} finally {
EXCEPTION_LINE_NUMBER = lineNumber;
}
}
@Test
public void testJSException() throws IOException {
try (Context context = JSTest.newContextBuilder().build()) {
context.eval(Source.newBuilder(ID, "\nisNotDefined.doesNotMakeSense = 1;\n", "isnotdefined.js").build());
fail("PolyglotException not thrown");
} catch (PolyglotException e) {
assertTrue(e.isGuestException());
assertEquals("ReferenceError: isNotDefined is not defined", e.getMessage());
StackTraceElement[] stackTrace = e.getStackTrace();
assertEquals("isnotdefined.js", stackTrace[0].getFileName());
assertEquals(2, stackTrace[0].getLineNumber());
}
}
@Test
public void testNestedJSException() throws IOException {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(graalEngine).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS(graalEngine, hostAccess);
bindings.putMember("objectFromJava", objectFromJava);
context.eval(Source.newBuilder(ID, "\nobjectFromJava.methodThatThrowsJSException();\n", "testmain.js").build());
fail("PolyglotException not thrown");
} catch (PolyglotException e) {
assertTrue(e.isHostException());
assertTrue(e.asHostException() instanceof PolyglotException);
assertTrue(((PolyglotException) e.asHostException()).isGuestException());
assertEquals("ReferenceError: isNotDefined is not defined", e.getMessage());
StackTraceElement[] stackTrace = Arrays.stream(e.getStackTrace()).filter(ste -> ste.getClassName().equals("<js>")).toArray(StackTraceElement[]::new);
assertEquals("nestedisnotdefined.js", stackTrace[0].getFileName());
assertEquals(2, stackTrace[0].getLineNumber());
assertEquals("testmain.js", stackTrace[1].getFileName());
assertEquals(2, stackTrace[1].getLineNumber());
}
}
@Test
@SuppressWarnings("RedundantOperationOnEmptyContainer")
public void testCaughtJSException() throws IOException {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(graalEngine).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS(graalEngine, hostAccess);
bindings.putMember("objectFromJava", objectFromJava);
Value val = context.eval(Source.newBuilder(ID, "var ex;\n" +
"try {\n" +
" isNotDefined.doesNotMakeSense = 1;\n" +
"} catch (e) {\n" +
" objectFromJava.methodWithMapArgument(e);\n" +
" ex = e;" +
"}\n" +
"ex;\n", "isnotdefinedcaught.js").build());
assertTrue(val.hasMembers());
assertTrue(val.getMemberKeys().isEmpty());
assertTrue(val.getMember("name").isString());
assertEquals("ReferenceError", val.getMember("name").asString());
assertTrue(val.getMember("message").isString());
assertEquals("isNotDefined is not defined", val.getMember("message").asString());
assertTrue(val.getMember("stack").isString());
assertEquals("ReferenceError: isNotDefined is not defined\n" +
" at isnotdefinedcaught.js:3:5", val.getMember("stack").asString());
assertTrue(objectFromJava.map.keySet().isEmpty());
assertEquals("ReferenceError", objectFromJava.map.get("name"));
assertEquals("isNotDefined is not defined", objectFromJava.map.get("message"));
assertEquals("ReferenceError: isNotDefined is not defined\n" +
" at isnotdefinedcaught.js:3:5", objectFromJava.map.get("stack"));
}
}
@Test
public void testJavaExceptionThroughJS() throws IOException {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(graalEngine).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS(graalEngine, hostAccess);
bindings.putMember("objectFromJava", objectFromJava);
context.eval(Source.newBuilder(ID, "\nobjectFromJava.methodThatThrowsException();\n", "hostexception.js").build());
fail("PolyglotException not thrown");
} catch (PolyglotException e) {
assertEquals("This operation is not supported!", e.getMessage());
StackTraceElement[] stackTrace = e.getStackTrace();
assertEquals(this.getClass().getSimpleName() + ".java", stackTrace[0].getFileName());
assertEquals(EXCEPTION_LINE_NUMBER, stackTrace[0].getLineNumber());
assertEquals("hostexception.js", stackTrace[1].getFileName());
assertEquals(2, stackTrace[1].getLineNumber());
assertTrue(e.isHostException());
Throwable hostException = e.asHostException();
assertTrue(hostException instanceof UnsupportedOperationException);
StackTraceElement[] hostExceptionStackTrace = hostException.getStackTrace();
assertEquals("This operation is not supported!", hostException.getMessage());
assertEquals(this.getClass().getSimpleName() + ".java", hostExceptionStackTrace[0].getFileName());
assertEquals(EXCEPTION_LINE_NUMBER, hostExceptionStackTrace[0].getLineNumber());
}
}
@Test
public void testNestedJavaExceptionThroughJS() throws IOException {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(graalEngine).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS(graalEngine, hostAccess);
bindings.putMember("objectFromJava", objectFromJava);
context.eval(Source.newBuilder(ID, "\nobjectFromJava.methodThatThrowsNestedJavaException();\n", "testmain" +
".js").build());
fail("PolyglotException not thrown");
} catch (PolyglotException e) {
assertEquals("This operation is not supported!", e.getMessage());
StackTraceElement[] stackTrace = e.getStackTrace();
assertEquals(this.getClass().getSimpleName() + ".java", stackTrace[0].getFileName());
assertEquals(EXCEPTION_LINE_NUMBER, stackTrace[0].getLineNumber());
StackTraceElement[] jsStackTrace = Arrays.stream(e.getStackTrace()).filter(ste -> ste.getClassName().equals("<js>")).toArray(StackTraceElement[]::new);
assertEquals("nestedhostexception.js", jsStackTrace[0].getFileName());
assertEquals(2, jsStackTrace[0].getLineNumber());
assertEquals("testmain.js", jsStackTrace[1].getFileName());
assertEquals(2, jsStackTrace[1].getLineNumber());
assertTrue(e.isHostException());
Throwable hostException = e.asHostException();
assertTrue(hostException instanceof UnsupportedOperationException);
StackTraceElement[] hostExceptionStackTrace = hostException.getStackTrace();
assertEquals("This operation is not supported!", hostException.getMessage());
assertEquals(this.getClass().getSimpleName() + ".java", hostExceptionStackTrace[0].getFileName());
assertEquals(EXCEPTION_LINE_NUMBER, hostExceptionStackTrace[0].getLineNumber());
}
}
@Test
public void testCaughtJavaException() throws IOException, NoSuchMethodException {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).allowAccess(Throwable.class.getMethod("getMessage")).allowAccess(
Object.class.getMethod("getClass")).allowAccess(Class.class.getMethod("getName")).build();
try (Engine graalEngine = JSTest.newEngineBuilder().build(); Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).engine(graalEngine).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS(graalEngine, hostAccess);
bindings.putMember("objectFromJava", objectFromJava);
Value val = context.eval(Source.newBuilder(ID, "var ex;\n" +
"try {\n" +
" objectFromJava.methodThatThrowsException();\n" +
"} catch (e) {\n" +
" objectFromJava.methodWithExceptionArgument(e, e.getClass().getName(), e.getMessage());\n" +
" ex = e;" +
"}\n" +
"ex;\n", "isnotdefinedcaught.js").build());
assertTrue(val.isHostObject());
assertTrue(val.isException());
assertEquals(objectFromJava.exception, val.asHostObject());
assertTrue(val.asHostObject() instanceof UnsupportedOperationException);
Exception hostException = val.asHostObject();
StackTraceElement[] hostExceptionStackTrace = hostException.getStackTrace();
assertEquals("This operation is not supported!", hostException.getMessage());
assertEquals(this.getClass().getSimpleName() + ".java", hostExceptionStackTrace[0].getFileName());
assertEquals(EXCEPTION_LINE_NUMBER, hostExceptionStackTrace[0].getLineNumber());
assertEquals("isnotdefinedcaught.js", objectFromJava.jsStackTrace[0].getFileName());
assertEquals(3, objectFromJava.jsStackTrace[0].getLineNumber());
assertEquals(objectFromJava.exception.getClass().getName(), objectFromJava.exceptionName);
assertEquals(objectFromJava.exception.getMessage(), objectFromJava.exceptionMessage);
}
}
}