package com.oracle.truffle.js.test.interop;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;
import org.graalvm.polyglot.proxy.ProxyObject;
import org.junit.Test;
import com.oracle.truffle.js.test.JSTest;
import com.oracle.truffle.js.test.polyglot.ForeignTestMap;
public class InteropArrayTest {
@Test
public void testArrayGetMembers() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "[3, 4, 1, 5]");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(1, array.getMember("2").asInt());
assertEquals(1, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testSlowArrayGetMembers() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "var a = [3, 4, 1, 5]; Object.defineProperty(a, 2, {get: function(){return" +
" 42;}}); a;");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(42, array.getMember("2").asInt());
assertEquals(42, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testSlowArrayWithIntKeyEnumerablePropertyGetMembers1() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "var a = [3, 4, 1, 5]; Object.defineProperty(a, 2, {get: function(){return" +
" 42;}, enumerable: true}); a;");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(42, array.getMember("2").asInt());
assertEquals(42, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testSlowArrayWithIntKeyEnumerablePropertyGetMembers2() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "var a = [3, 4, 1, 5]; Object.defineProperty(a, '2', {get: function()" +
"{return 42;}, enumerable: true}); a;");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(42, array.getMember("2").asInt());
assertEquals(42, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testSlowArrayWithStringKeyEnumerablePropertyGetMembers() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "var a = [3, 4, 1, 5]; Object.defineProperty(a, 'x', {get: function()" +
"{return 42;}, enumerable: true}); a;");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(1, array.getMember("2").asInt());
assertEquals(1, array.getArrayElement(2).asInt());
assertEquals(42, array.getMember("x").asInt());
assertEquals(array.getMemberKeys().toString(), Collections.singleton("x"), array.getMemberKeys());
}
}
@Test
public void testTypedArrayGetMembers() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "Int8Array.from([3, 4, 1, 5]);");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(1, array.getMember("2").asInt());
assertEquals(1, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testArgumentsObjectGetMembers() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "(function(){return arguments;})(3, 4, 1, 5);");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getMember("0").asInt());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(1, array.getMember("2").asInt());
assertEquals(1, array.getArrayElement(2).asInt());
assertTrue(array.getMemberKeys().toString(), array.getMemberKeys().isEmpty());
}
}
@Test
public void testArrayHoles() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "[3,,,5]");
assertEquals(4, array.getArraySize());
assertEquals(3, array.getArrayElement(0).asInt());
assertEquals(5, array.getArrayElement(3).asInt());
assertTrue(array.getArrayElement(1).isNull());
assertTrue(array.getArrayElement(2).isNull());
array.setArrayElement(1, 4);
array.setArrayElement(2, 1);
assertEquals(4, array.getArrayElement(1).asInt());
assertEquals(1, array.getArrayElement(2).asInt());
}
}
@Test(expected = ArrayIndexOutOfBoundsException.class)
public void testArrayIndexOutOfBounds() {
try (Context context = JSTest.newContextBuilder().build()) {
Value array = context.eval(ID, "[3, 4, 1, 5]");
assertEquals(4, array.getArraySize());
array.getArrayElement(4);
}
}
private static final int[] JAVA_ARRAY = new int[]{3, 4, 1, 5};
private static final List<Integer> JAVA_LIST = Arrays.stream(JAVA_ARRAY).boxed().collect(Collectors.toList());
private static final String JS_ARRAY_STRING = Arrays.toString(JAVA_ARRAY);
public static class ToBePassedToJS {
private List<?> list;
@HostAccess.Export
public void methodWithListArgument(List<?> argList) {
this.list = argList;
}
@HostAccess.Export
public void methodWithArrayArgument(int[] argArray) {
this.list = Arrays.stream(argArray).boxed().collect(Collectors.toList());
}
@HostAccess.Export
public int[] methodThatReturnsArray() {
return JAVA_ARRAY;
}
@HostAccess.Export
public Value methodThatReturnsArrayAsValue() {
return Value.asValue(JAVA_ARRAY);
}
@HostAccess.Export
public List<Integer> methodThatReturnsList() {
return JAVA_LIST;
}
@HostAccess.Export
public Object createForeignMap() {
final ForeignTestMap map = new ForeignTestMap();
map.getContainer().put("x", 42);
map.getContainer().put("y", "foo");
return map;
}
@HostAccess.Export
public Object[] methodThatReturnsArrayWithJSObject(Object jsObject) {
return new Object[]{41, jsObject, "string", createForeignMap()};
}
}
@Test
public void testArrayBasic() {
try (Context context = JSTest.newContextBuilder().build()) {
Value v = context.eval(ID, JS_ARRAY_STRING);
commonCheck(v);
}
}
private static void commonCheck(Value v) {
assertEquals(JAVA_ARRAY.length, v.getArraySize());
assertArrayEquals(JAVA_ARRAY,
IntStream.range(0, (int) v.getArraySize()).map(i -> v.getArrayElement(i).asInt()).toArray());
}
@Test
public void testArrayAsListParameter() {
testArrayAsParameter("methodWithListArgument");
}
@Test
public void testArrayAsArrayParameter() {
testArrayAsParameter("methodWithArrayArgument");
}
private static void testArrayAsParameter(String methodName) {
final HostAccess hostAccess = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class).build();
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS();
bindings.putMember("objectFromJava", objectFromJava);
context.eval(ID, "objectFromJava." + methodName + "(" + JS_ARRAY_STRING + ")");
assertEquals(JAVA_ARRAY.length, objectFromJava.list.size());
assertEquals(JS_ARRAY_STRING, Arrays.toString(objectFromJava.list.toArray()));
}
}
@Test
public void testJavaArrayAsJSArray() {
final HostAccess hostAccess = HostAccess.newBuilder().allowArrayAccess(true).build();
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).build()) {
Value bindings = context.getBindings(ID);
bindings.putMember("arrayFromJava", JAVA_ARRAY);
Value v = context.eval(ID, "var recreatedArray = [];" +
"for (var i = 0; i < arrayFromJava.length; i++)" +
"recreatedArray.push(arrayFromJava[i]);" +
"recreatedArray");
commonCheck(v);
}
}
@Test
public void testJavaListAsJSArray() {
final HostAccess hostAccess = HostAccess.newBuilder().allowListAccess(true).build();
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).build()) {
Value bindings = context.getBindings(ID);
bindings.putMember("arrayFromJava", JAVA_LIST);
Value v = context.eval(ID, "var recreatedArray = [];" +
"for (var i = 0; i < arrayFromJava.length; i++)" +
"recreatedArray.push(arrayFromJava[i]);" +
"recreatedArray");
commonCheck(v);
}
}
@Test
public void testJavaReturnArrayAsJSArray() {
testJavaReturnArrayOrListAsJSArray(true);
}
@Test
public void testJavaReturnListAsJSArray() {
testJavaReturnArrayOrListAsJSArray(false);
}
private static void testJavaReturnArrayOrListAsJSArray(boolean isArray) {
final HostAccess.Builder hostAccessBuilder = HostAccess.newBuilder().allowAccessAnnotatedBy(HostAccess.Export.class);
final HostAccess hostAccess = (isArray ? hostAccessBuilder.allowArrayAccess(true) : hostAccessBuilder.allowListAccess(true)).build();
String methodName = isArray ? "methodThatReturnsArray" : "methodThatReturnsList";
try (Context context = JSTest.newContextBuilder().allowHostAccess(hostAccess).build()) {
Value bindings = context.getBindings(ID);
ToBePassedToJS objectFromJava = new ToBePassedToJS();
bindings.putMember("objectFromJava", objectFromJava);
Value v = context.eval(ID, "var arrayFromJava = objectFromJava." + methodName + "();" +
"var recreatedArray = [];" +
"for (var i = 0; i < arrayFromJava.length; i++)" +
"recreatedArray.push(arrayFromJava[i]);" +
"recreatedArray");
commonCheck(v);
}
}
@Test
public void testPrintJavaArrayInJS() {
HostAccess accessWithArrays = HostAccess.newBuilder(HostAccess.EXPLICIT).allowArrayAccess(true).build();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (Context context = JSTest.newContextBuilder().allowHostAccess(accessWithArrays).out(baos).err(baos).build()) {
context.getBindings(ID).putMember("javaArray", new ToBePassedToJS());
context.eval(ID, "var arrayFromJava = javaArray.methodThatReturnsArrayWithJSObject(" +
"{foo: 'bar', number: 42, f: function() { return 'yes';}, array: [2, 4, 8]});" +
"console.log(arrayFromJava);");
assertEquals("[41, {foo: \"bar\", number: 42, f: function() { return 'yes';}, array: [2, 4, 8]}, \"string\", {x: 42, y: \"foo\"}]", baos.toString().trim());
baos.reset();
context.eval(ID, "var arrayFromJava = javaArray.methodThatReturnsArray();" +
"console.log(arrayFromJava);");
assertEquals("[3, 4, 1, 5]", baos.toString().trim());
baos.reset();
context.eval(ID, "var arrayFromJavaAsValue = javaArray.methodThatReturnsArrayAsValue();" +
"console.log(arrayFromJavaAsValue);");
assertEquals("[3, 4, 1, 5]", baos.toString().trim());
}
}
@Test
public void testForLetItemOfLazyArray() {
try (Context context = JSTest.newContextBuilder().build()) {
Value collect = context.eval(ID, "" +
"(function (arr) {\n" +
" let collect = [];\n" +
" for (let item of arr) {\n" +
" collect.push(item);\n" +
" collect.push(item);\n" +
" collect.push(item);\n" +
" }\n" +
" return collect;\n" +
"})\n"
);
final List<Integer> list = Arrays.asList(5, 7, 11, 13, 17, 23);
Value tripples = collect.execute(new LazyArray(list.iterator()));
assertTrue("Array returned", tripples.hasArrayElements());
assertEquals(list.size() * 3, tripples.getArraySize());
}
}
@Test
public void testManyTypes() {
try (Context context = JSTest.newContextBuilder().build()) {
Value arrays = context.eval(ID, "var arrays = [];" +
"var a;" +
"a = [1,2,3,4];" +
"arrays.push(a);" +
"a = [1000,2000,3000];" +
"arrays.push(a);" +
"a = [1.1,2.2,3.3];" +
"arrays.push(a);" +
"a = ['a','b','c'];" +
"arrays.push(a);" +
"a = [1,2,3,4]; a.push(5);" +
"arrays.push(a);" +
"a = [1000,2000,3000]; a.push(4000);" +
"arrays.push(a);" +
"a = [1.1,2.2,3.3]; a.push(4.4);" +
"arrays.push(a);" +
"a = ['a','b','c']; a.push('d');" +
"arrays.push(a);" +
"a = [,,1,2,3,4]; delete a[4];" +
"arrays.push(a);" +
"a = [1,,2,,3]; a.push(4);" +
"arrays.push(a);" +
"a = ['a',,'b',,'c']; a.push('d');" +
"arrays.push(a);" +
"a = [1]; a[2**31] = 2;" +
"arrays.push(a);" +
"arrays;");
for (int i = 0; i < arrays.getArraySize(); i++) {
Value array = arrays.getArrayElement(i);
assertTrue(array.hasArrayElements());
array.setArrayElement(0, i);
}
}
}
@Test
public void testArrayFromForeignArrayLike() {
try (Context context = JSTest.newContextBuilder().build()) {
Map<String, Object> map = new HashMap<>();
map.put("length", 2);
map.put("0", 42);
map.put("1", 211);
Object arrayLike = ProxyObject.fromMap(map);
context.getBindings(ID).putMember("arrayLike", arrayLike);
Value result = context.eval(ID, "var array = Array.from(arrayLike); array.length === 2 && array[0] === 42 && array[1] === 211;");
assertTrue(result.isBoolean());
assertTrue(result.asBoolean());
}
}
private static final class LazyArray implements ProxyArray {
private final Iterator<?> it;
private long at;
LazyArray(Iterator<?> it) {
this.it = it;
this.at = 0;
}
@Override
public Object get(long index) {
if (index == at) {
at++;
return it.next();
}
return null;
}
@Override
public void set(long index, Value value) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(long index) {
throw new UnsupportedOperationException();
}
@Override
public long getSize() {
return it.hasNext() ? at + 1 : at;
}
}
}