package com.oracle.truffle.js.test.interop;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.function.Consumer;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;
import org.junit.Test;
import com.oracle.truffle.js.test.JSTest;
public class JavaScriptHostInteropTest {
public interface MyHostIntf {
int foo(int value);
Map<?, ?>[] bounce(Map<?, ?>[] mapList);
void write(String a, String b, String c);
}
public static class MyHostObj implements MyHostIntf {
@Override
public int foo(int value) {
return value;
}
@Override
public Map<?, ?>[] bounce(Map<?, ?>[] mapList) {
return mapList;
}
@Override
public void write(@SuppressWarnings("unused") String a, @SuppressWarnings("unused") String b, @SuppressWarnings("unused") String c) {
}
}
@Test
public void testArityError() {
Consumer<PolyglotException> expectedException;
try (Context context = JSTest.newContextBuilder().allowHostAccess(HostAccess.ALL).build()) {
for (int i = 0; i < 2; i++) {
Object hostObj;
if (i == 0) {
hostObj = new MyHostObj();
} else {
Object delegate = new MyHostObj();
hostObj = Proxy.newProxyInstance(MyHostIntf.class.getClassLoader(),
new Class[]{MyHostIntf.class}, (proxy, method, args) -> method.invoke(delegate, args));
}
context.getBindings(ID).putMember("hostobj", hostObj);
String expectedClassName = hostObj.getClass().getSimpleName();
expectedException = e -> {
assertThat(e.getMessage(), e.getMessage(), containsString(expectedClassName));
assertThat(e.getMessage(), e.getMessage(), containsString("foo"));
assertThat(e.getMessage(), e.getMessage(), containsString("Arity error"));
};
assertThrows(() -> context.eval(ID, "hostobj.foo();"), expectedException);
assertThrows(() -> context.eval(ID, "" +
"var foo = hostobj.foo;\n" +
"foo();"),
expectedException);
expectedException = e -> {
assertThat(e.getMessage(), e.getMessage(), containsString(expectedClassName));
assertThat(e.getMessage(), e.getMessage(), containsString("write"));
assertThat(e.getMessage(), e.getMessage(), containsString("Arity error"));
};
assertThrows(() -> context.eval(ID, "hostobj.write('a', 'b')"), expectedException);
assertThrows(() -> context.eval(ID, "hostobj['write']('a', 'b')"), expectedException);
assertThrows(() -> context.eval(ID, "hostobj.write('a', 'b', 'c', 'd')"), expectedException);
assertThrows(() -> context.eval(ID, "hostobj['write']('a', 'b', 'c', 'd')"), expectedException);
}
}
}
@Test
public void testArgumentTypeError() {
Consumer<PolyglotException> expectedException;
try (Context context = JSTest.newContextBuilder().allowHostAccess(HostAccess.ALL).build()) {
for (int i = 0; i < 2; i++) {
Object hostObj;
if (i == 0) {
hostObj = new MyHostObj();
} else {
Object delegate = new MyHostObj();
hostObj = Proxy.newProxyInstance(MyHostIntf.class.getClassLoader(),
new Class[]{MyHostIntf.class}, (proxy, method, args) -> method.invoke(delegate, args));
}
context.getBindings(ID).putMember("hostobj", hostObj);
String expectedClassName = hostObj.getClass().getSimpleName();
expectedException = e -> {
assertThat(e.getMessage(), e.getMessage(), containsString(expectedClassName));
assertThat(e.getMessage(), e.getMessage(), containsString("foo"));
assertThat(e.getMessage(), e.getMessage(), containsString("Cannot convert '{}'(language: JavaScript, type: Object) to Java type 'int'"));
};
assertThrows(() -> context.eval(ID, "hostobj.foo({});"), expectedException);
assertThrows(() -> context.eval(ID, "" +
"var foo = hostobj.foo;\n" +
"foo({});"), expectedException);
context.eval(ID, "hostobj.foo(42);");
context.eval(ID, "" +
"var foo = hostobj.foo;\n" +
"foo(42);");
expectedException = e -> {
assertThat(e.getMessage(), e.getMessage(), containsString(expectedClassName));
assertThat(e.getMessage(), e.getMessage(), containsString("bounce"));
assertThat(e.getMessage(), e.getMessage(), containsString("Cannot convert 'abc'(language: Java, type: java.lang.String) to Java type 'java.util.Map[]'"));
};
assertThrows(() -> context.eval(ID, "hostobj.bounce('abc')"), expectedException);
assertThrows(() -> context.eval(ID, "hostobj['bounce']('abc')"), expectedException);
expectedException = e -> {
assertThat(e.getMessage(), e.getMessage(), containsString(expectedClassName));
assertThat(e.getMessage(), e.getMessage(), containsString("write"));
assertThat(e.getMessage(), e.getMessage(),
containsString("Cannot convert '6'(language: Java, type: java.lang.Integer) to Java type 'java.lang.String'"));
};
assertThrows(() -> context.eval(ID, "hostobj.write(6, 'eight', null)"), expectedException);
assertThrows(() -> context.eval(ID, "hostobj['write'](6, 'eight', null)"), expectedException);
context.eval(ID, "hostobj.write(JSON.stringify(6), 'eight', null)");
context.eval(ID, "hostobj['write'](JSON.stringify(6), 'eight', null)");
}
}
}
static final String EXPECTED_EXCEPTION_MESSAGE = "This will be swallowed :(";
public static class Hello {
public void thisIsFine(Value human) {
assertEquals("timmy", human.invokeMember("hello").asString());
}
public void thisWillBreak(@SuppressWarnings("unused") Object human) {
throw new ClassCastException(EXPECTED_EXCEPTION_MESSAGE);
}
}
@Test
public void testHostClassCastException() {
try (Context context = Context.newBuilder().allowHostAccess(HostAccess.ALL).build()) {
context.getBindings(ID).putMember("hello", new Hello());
try {
context.eval(ID, "class Human {\n" +
" constructor(name) {\n" +
" this.name = name;\n" +
" }\n" +
"\n" +
" hello() {\n" +
" return this.name;\n" +
" }\n" +
"}\n" +
"\n" +
"const human = new Human(\"timmy\");\n" +
"\n" +
"hello.thisIsFine(human);\n" +
"hello.thisWillBreak(human);");
fail("should have thrown");
} catch (PolyglotException e) {
assertTrue(e.isHostException());
assertEquals(ClassCastException.class, e.asHostException().getClass());
assertEquals(EXPECTED_EXCEPTION_MESSAGE, e.asHostException().getMessage());
}
}
}
private static void assertThrows(Runnable test, Consumer<PolyglotException> exceptionVerifier) {
try {
test.run();
fail("should have thrown");
} catch (PolyglotException e) {
assertTrue(e.isGuestException());
exceptionVerifier.accept(e);
}
}
@Test
public void hostObjectIdentity() {
try (Context context = JSTest.newContextBuilder().allowHostAccess(HostAccess.ALL).build()) {
Object proxy1 = new Object();
Object proxy2 = new Object();
Value equals = context.eval(ID, "(function(a, b){return a == b;})");
Value identical = context.eval(ID, "(function(a, b){return a === b;})");
assertTrue(equals.execute(proxy1, proxy1).asBoolean());
assertTrue(identical.execute(proxy1, proxy1).asBoolean());
assertFalse(equals.execute(proxy1, proxy2).asBoolean());
assertFalse(identical.execute(proxy1, proxy2).asBoolean());
}
}
}