package com.oracle.truffle.js.test;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.junit.Assume;
import com.oracle.truffle.api.RootCallTarget;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.DynamicObjectLibrary;
import com.oracle.truffle.js.lang.JavaScriptLanguage;
import com.oracle.truffle.js.nodes.ScriptNode;
import com.oracle.truffle.js.nodes.function.JSFunctionExpressionNode;
import com.oracle.truffle.js.parser.JSParser;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.JSContextOptions;
import com.oracle.truffle.js.runtime.JSException;
import com.oracle.truffle.js.runtime.JSRealm;
import com.oracle.truffle.js.runtime.builtins.JSArray;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.builtins.JSFunctionData;
import com.oracle.truffle.js.runtime.objects.JSLazyString;
import com.oracle.truffle.js.runtime.objects.Null;
public class TestHelper implements AutoCloseable {
private static final double DELTA = 1e-15;
private final Context.Builder contextBuilder;
private Context ctx;
public TestHelper() {
this(JSTest.newContextBuilder().option(JSContextOptions.DEBUG_BUILTIN_NAME, "true"));
}
public TestHelper(Context.Builder contextBuilder) {
this.contextBuilder = contextBuilder;
}
@Override
public void close() {
if (ctx != null) {
ctx.close();
ctx = null;
}
}
public Context getPolyglotContext() {
if (ctx == null) {
ctx = contextBuilder.build();
}
return ctx;
}
public JSContext getJSContext() {
return getRealm().getContext();
}
public JSRealm getRealm() {
return JavaScriptLanguage.getJSRealm(getPolyglotContext());
}
public DynamicObject getGlobalObject() {
return getRealm().getGlobalObject();
}
public Object getBinding(String key) {
return DynamicObjectLibrary.getUncached().getOrDefault(getGlobalObject(), key, null);
}
public void putBinding(String key, Object value) {
DynamicObjectLibrary.getUncached().putIfPresent(getGlobalObject(), key, value);
}
public Object run(String sourceCode) {
return toHostValue(runValue(sourceCode));
}
public double runDouble(String sourceCode) {
return runValue(sourceCode).asDouble();
}
public boolean runBoolean(String sourceCode) {
return runValue(sourceCode).asBoolean();
}
public Value runValue(String sourceCode) {
Source source = Source.create(JavaScriptLanguage.ID, sourceCode);
return getPolyglotContext().eval(source);
}
public void runVoid(String sourceCode) {
runValue(sourceCode);
}
public boolean runExpectUndefined(String sourceCode) {
Value result = runValue(sourceCode);
Source checkUndefined = Source.create(JavaScriptLanguage.ID, "(function(arg) { return arg === undefined; });");
Value fnCheckUndefined = getPolyglotContext().eval(checkUndefined);
return fnCheckUndefined.execute(result).asBoolean();
}
@SuppressWarnings("deprecation")
public void runExpectSyntaxError(String sourceCode) {
enterContext();
try {
getParser().parseScript(getJSContext(), sourceCode);
fail("expected syntax error to be thrown");
} catch (JSException e) {
assertTrue(e.isSyntaxError());
} finally {
leaveContext();
}
}
public Object runNoPolyglot(String source) {
enterContext();
Object result = null;
try {
ScriptNode program = getParser().parseScript(getJSContext(), source);
result = runNoPolyglot(program);
} finally {
leaveContext();
}
return result;
}
public Object runNoPolyglot(ScriptNode scriptNode) {
return transformResult(scriptNode.run(getRealm()));
}
private static Object transformResult(Object value) {
if (value instanceof JSLazyString) {
return value.toString();
}
return value;
}
public Value runRedirectOutput(String sourceCode, PrintStream writer, PrintStream errorWriter, boolean isInteractive, Map<String, Object> bindings) {
Context specialCtx = JSTest.newContextBuilder().out(writer).err(errorWriter).build();
Value jsBindings = specialCtx.getBindings(JavaScriptLanguage.ID);
for (Map.Entry<String, Object> entry : bindings.entrySet()) {
jsBindings.putMember(entry.getKey(), entry.getValue());
}
Source source = Source.newBuilder(JavaScriptLanguage.ID, sourceCode, "TestCase").interactive(isInteractive).buildLiteral();
return specialCtx.eval(source);
}
public String runToString(String source) {
return runToString(source, false);
}
public String runToString(String source, boolean isInteractive) {
return runToString(source, isInteractive, Collections.emptyMap());
}
public String runToString(String source, boolean isInteractive, Map<String, Object> bindings) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PrintStream stream = new PrintStream(baos, false, "UTF-8")) {
runRedirectOutput(source, stream, stream, isInteractive, bindings);
}
return baos.toString(StandardCharsets.UTF_8.displayName()).replaceAll("\r\n", "\n");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public Object runSilent(String sourceCode) {
OutputStream out = new OutputStream() {
@Override
public void write(int b) throws IOException {
}
};
PrintStream stream = new PrintStream(out);
return toHostValue(runRedirectOutput(sourceCode, stream, stream, false, Collections.emptyMap()));
}
public DynamicObject runJSArray(String source) {
Object obj = runNoPolyglot(source);
assert JSArray.isJSArray(obj);
return (DynamicObject) obj;
}
public class ParsedFunction {
private final JSFunctionData functionData;
public ParsedFunction(JSFunctionData functionData) {
this.functionData = functionData;
}
public Object call(Object[] args) {
DynamicObject funObj = JSFunction.create(getRealm(), functionData);
return JSFunction.call(funObj, Null.instance, args);
}
public RootNode getRootNode() {
return ((RootCallTarget) functionData.getCallTarget()).getRootNode();
}
}
public ParsedFunction parseFirstFunction(String source) {
return new ParsedFunction(findFirstNodeInstance(parse(source).getRootNode(), JSFunctionExpressionNode.class).getFunctionData());
}
private static <T> T findFirstNodeInstance(Node root, Class<T> clazz) {
if (clazz.isInstance(root)) {
return clazz.cast(root);
}
for (Node child : root.getChildren()) {
T node = findFirstNodeInstance(child, clazz);
if (node != null) {
return node;
}
}
return null;
}
public static void assertNumberEquals(Number expected, Object actual) {
assertThat(actual, instanceOf(Number.class));
assertEquals(expected.doubleValue(), ((Number) actual).doubleValue(), DELTA);
}
private JSParser getParser() {
return (JSParser) getJSContext().getEvaluator();
}
public ScriptNode parse(String script) {
return getParser().parseScript(getJSContext(), script);
}
public static Object toHostValue(final Value value) {
if (value.isBoolean()) {
return value.asBoolean();
}
if (value.isHostObject()) {
return value.asHostObject();
}
if (value.isNull()) {
return null;
}
if (value.isNumber()) {
if (value.fitsInInt()) {
return value.asInt();
}
if (value.fitsInDouble()) {
return value.asDouble();
}
return new AssertionError("Unknown number value: " + value);
}
if (value.isString()) {
return value.asString();
}
throw new AssertionError("Unknown value: " + value);
}
public void enterContext() {
getPolyglotContext().enter();
}
public void leaveContext() {
getPolyglotContext().leave();
}
public void assumeES6OrLater() {
enterContext();
Assume.assumeTrue(getJSContext().getEcmaScriptVersion() >= 6);
leaveContext();
}
}