/*
* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.truffle.tck;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.interop.ForeignAccess;
import com.oracle.truffle.api.interop.InteropException;
import com.oracle.truffle.api.interop.Message;
import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.tck.TruffleRunner.Inject;
import com.oracle.truffle.tck.TruffleRunner.Warmup;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.Runner;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters;
import org.junit.runners.parameterized.ParametersRunnerFactory;
import org.junit.runners.parameterized.TestWithParameters;
/**
* JUnit test runner for unit testing Truffle AST interpreters.
* <p>
* A test using {@link TruffleRunner} consists of 2 parts, a Truffle AST to be tested, and a test
* method that drives the test, provides input argument values and validates the result.
*
* <h4>Writing a test AST</h4>
*
* The Truffle AST to be tested is written as a {@link RootNode} subclass, for example:
* <p>
* {@codesnippet TruffleRunnerSnippets#TestExecuteNode}
*
* <h4>Writing a test method</h4>
*
* The test method is a normal method annotated with {@link Test}. It may have one or more arguments
* of type {@link CallTarget} that are annotated with {@link Inject}. The {@link Inject} annotation
* specifies a {@link RootNode} subclass that is the root of a test AST, and the
* {@link TruffleRunner} will create one {@link CallTarget} for each of these test ASTs. The test
* method can then execute the AST by calling the {@link CallTarget#call} method.
* <p>
* Typically a test method will prepare some arguments, and then do a single call to
* {@link CallTarget#call}. Then it should verify the result by inspecting the return value and
* checking the expected side effects of the test code.
* <p>
* {@codesnippet TruffleRunnerSnippets#ExampleTest}
*
* @see Warmup warmup iterations and compilation
* @see ParametersFactory parameterized Truffle AST tests
*
* @since 0.25
*/
public final class TruffleRunner extends BlockJUnit4ClassRunner {
private static final TruffleTestInvoker<?> truffleTestInvoker = TruffleTestInvoker.create();
/**
* A parameter annotated with {@link Inject} specifies the {@link RootNode} of the test AST.
*
* @see TruffleRunner
*
* @since 0.25
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Inject {
/**
* Defines the {@link RootNode root node} of the Truffle tree that should be tested.
*/
Class<? extends RootNode> value();
}
/**
* A test method can be annotated with {@link Warmup} to specify how many warmup iterations of a
* test should be done before the Truffle tree is compiled. If this annotation is missing, the
* default value of 3 is used.
* <p>
* {@codesnippet TruffleRunnerSnippets#warmupTest}
* <p>
* In this example, the test code will in total be run 6 times. The first 5 iterations are
* warmup. The {@link CallTarget#call} invocation will run in the interpreter, simply calling
* the {@link RootNode#execute} method. This allows the AST to specialize itself before it is
* compiled.
* <p>
* After warmup, the resulting specialized AST is compiled, and in the final iteration the
* {@link CallTarget} represents the resulting compiled code.
*
* @see TruffleRunner
*
* @since 0.25
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Warmup {
/**
* The number of warmup iterations to run before a test is compiled.
*/
int value();
}
/**
* {@link ParametersRunnerFactory} for testing Truffle AST interpreters using
* {@link Parameterized} unit tests. To use the parameters for constructing the test AST, the
* test {@link RootNode} constructor may take the test class as single argument, or
* alternatively the test {@link RootNode} can be a non-static inner class of the test class.
* <p>
* {@codesnippet TruffleRunnerSnippets#ParameterizedTest}
*
* @see TruffleRunner
*
* @since 0.25
*/
public static final class ParametersFactory implements ParametersRunnerFactory {
/**
* Should not be called directly. To use this class, annotate your test class with
* {@code @Parameterized.UseParametersRunnerFactory(TruffleRunner.ParametersFactory.class)}.
*
* @see TruffleRunner
*
* @since 0.25
*/
public ParametersFactory() {
}
/**
* Internal method used by the JUnit framework. Do not call directly.
*
* @since 0.25
*/
@Override
public Runner createRunnerForTestWithParameters(TestWithParameters test) throws InitializationError {
return new ParameterizedRunner(test);
}
}
private static final class ParameterizedRunner extends BlockJUnit4ClassRunnerWithParameters {
ParameterizedRunner(TestWithParameters test) throws InitializationError {
super(test);
}
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
Statement ret = truffleTestInvoker.createStatement(getName(), getTestClass(), method, test);
if (ret == null) {
ret = super.methodInvoker(method, test);
}
return ret;
}
@Override
protected void validateTestMethods(List<Throwable> errors) {
TruffleTestInvoker.validateTestMethods(getTestClass(), errors);
}
}
/**
* Should not be called directly. To use this class, annotate your test class with
* {@code @RunWith(TruffleRunner.class)}.
*
* @see TruffleRunner
*
* @since 0.25
*/
public TruffleRunner(Class<?> klass) throws InitializationError {
super(klass);
}
/**
* Internal method used by the JUnit framework. Do not call directly.
*
* @since 0.25
*/
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
Statement ret = truffleTestInvoker.createStatement(testName(method), getTestClass(), method, test);
if (ret == null) {
ret = super.methodInvoker(method, test);
}
return ret;
}
/**
* Internal method used by the JUnit framework. Do not call directly.
*
* @since 0.25
*/
@Override
protected void validateTestMethods(List<Throwable> errors) {
TruffleTestInvoker.validateTestMethods(getTestClass(), errors);
}
}
class TruffleRunnerSnippets {
// Checkstyle: stop
// BEGIN: TruffleRunnerSnippets#TestExecuteNode
public class TestExecuteNode extends RootNode {
@Child Node executeNode;
public TestExecuteNode() {
super(null);
executeNode = Message.createExecute(1).createNode();
}
@Override
public Object execute(VirtualFrame frame) {
TruffleObject obj = (TruffleObject) frame.getArguments()[0];
try {
return ForeignAccess.sendExecute(executeNode, obj);
} catch (InteropException ex) {
CompilerDirectives.transferToInterpreter();
Assert.fail(ex.getMessage());
return null;
}
}
}
// END: TruffleRunnerSnippets#TestExecuteNode
// Checkstyle: resume
private static TruffleObject prepareArgumentValue() {
return null;
}
private static Object expectedRetValue() {
return null;
}
// BEGIN: TruffleRunnerSnippets#ExampleTest
@RunWith(TruffleRunner.class)
public class ExampleTest {
@Test
public void executeTest(@Inject(TestExecuteNode.class) CallTarget target) {
TruffleObject receiver = prepareArgumentValue();
Object ret = target.call(receiver);
Assert.assertEquals(expectedRetValue(), ret);
}
}
// END: TruffleRunnerSnippets#ExampleTest
// BEGIN: TruffleRunnerSnippets#warmupTest
@Test
@Warmup(5)
public void warmupTest(@Inject(TestExecuteNode.class) CallTarget target) {
TruffleObject receiver = prepareArgumentValue();
Object ret = target.call(receiver);
Assert.assertEquals(expectedRetValue(), ret);
}
// END: TruffleRunnerSnippets#warmupTest
// Checkstyle: stop
// BEGIN: TruffleRunnerSnippets#ParameterizedTest
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(TruffleRunner.ParametersFactory.class)
public static class ParameterizedTest {
@Parameters(name = "{0}, {1}")
public static Collection<Object[]> data() {
ArrayList<Object[]> ret = new ArrayList<>();
ret.add(new Object[]{5, "test"});
ret.add(new Object[]{-3, "asdf"});
return ret;
}
@Parameter(0) int intParam;
@Parameter(1) String stringParam;
public class TestConstArgNode extends RootNode {
private final int iArg;
private final String sArg;
@Child Node executeNode;
public TestConstArgNode() {
super(null);
this.iArg = intParam;
this.sArg = stringParam;
}
@Override
public Object execute(VirtualFrame frame) {
TruffleObject obj = (TruffleObject) frame.getArguments()[0];
try {
return ForeignAccess.sendExecute(executeNode, obj, iArg, sArg);
} catch (InteropException ex) {
CompilerDirectives.transferToInterpreter();
Assert.fail(ex.getMessage());
return null;
}
}
}
@Test
public void constArg(@Inject(TestConstArgNode.class) CallTarget target) {
TruffleObject arg = prepareArgumentValue();
Object ret = target.call(arg);
Assert.assertEquals(expectedRetValue(), ret);
}
}
// END: TruffleRunnerSnippets#ParameterizedTest
// Checkstyle: resume
}