/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.painless;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
/**
* Tests for Painless implementing different interfaces.
*/
public class ImplementInterfacesTests extends ScriptTestCase {
public interface NoArgs {
String[] ARGUMENTS = new String[] {};
Object execute();
}
public void testNoArgs() {
assertEquals(1, scriptEngine.compile(NoArgs.class, null, "1", emptyMap()).execute());
assertEquals("foo", scriptEngine.compile(NoArgs.class, null, "'foo'", emptyMap()).execute());
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
assertEquals("Variable [doc] is not defined.", e.getMessage());
// _score was once embedded into painless by deep magic
e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
assertEquals("Variable [_score] is not defined.", e.getMessage());
String debug = Debugger.toString(NoArgs.class, "int i = 0", new CompilerSettings());
/* Elasticsearch requires that scripts that return nothing return null. We hack that together by returning null from scripts that
* return Object if they don't return anything. */
assertThat(debug, containsString("ACONST_NULL"));
assertThat(debug, containsString("ARETURN"));
}
public interface OneArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object arg);
}
public void testOneArg() {
Object rando = randomInt();
assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));
rando = randomAlphaOfLength(5);
assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
assertEquals("Variable [doc] is not defined.", e.getMessage());
// _score was once embedded into painless by deep magic
e = expectScriptThrows(IllegalArgumentException.class, () ->
scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
assertEquals("Variable [_score] is not defined.", e.getMessage());
}
public interface ArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String[] arg);
}
public void testArrayArg() {
String rando = randomAlphaOfLength(5);
assertEquals(rando, scriptEngine.compile(ArrayArg.class, null, "arg[0]", emptyMap()).execute(new String[] {rando, "foo"}));
}
public interface PrimitiveArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(int[] arg);
}
public void testPrimitiveArrayArg() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(PrimitiveArrayArg.class, null, "arg[0]", emptyMap()).execute(new int[] {rando, 10}));
}
public interface DefArrayArg {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(Object[] arg);
}
public void testDefArrayArg() {
Object rando = randomInt();
assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
rando = randomAlphaOfLength(5);
assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
assertEquals(5, scriptEngine.compile(DefArrayArg.class, null, "arg[0].length()", emptyMap()).execute(new Object[] {rando, 10}));
}
public interface ManyArgs {
String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
Object execute(int a, int b, int c, int d);
boolean uses$a();
boolean uses$b();
boolean uses$c();
boolean uses$d();
}
public void testManyArgs() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(ManyArgs.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
assertEquals(10, scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));
// While we're here we can verify that painless correctly finds used variables
ManyArgs script = scriptEngine.compile(ManyArgs.class, null, "a", emptyMap());
assertTrue(script.uses$a());
assertFalse(script.uses$b());
assertFalse(script.uses$c());
assertFalse(script.uses$d());
script = scriptEngine.compile(ManyArgs.class, null, "a + b + c", emptyMap());
assertTrue(script.uses$a());
assertTrue(script.uses$b());
assertTrue(script.uses$c());
assertFalse(script.uses$d());
script = scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap());
assertTrue(script.uses$a());
assertTrue(script.uses$b());
assertTrue(script.uses$c());
assertTrue(script.uses$d());
}
public interface VarargTest {
String[] ARGUMENTS = new String[] {"arg"};
Object execute(String... arg);
}
public void testVararg() {
assertEquals("foo bar baz", scriptEngine.compile(VarargTest.class, null, "String.join(' ', Arrays.asList(arg))", emptyMap())
.execute("foo", "bar", "baz"));
}
public interface DefaultMethods {
String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
Object execute(int a, int b, int c, int d);
default Object executeWithOne() {
return execute(1, 1, 1, 1);
}
default Object executeWithASingleOne(int a, int b, int c) {
return execute(a, b, c, 1);
}
}
public void testDefaultMethods() {
int rando = randomInt();
assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).executeWithASingleOne(rando, 0, 0));
assertEquals(10, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));
assertEquals(4, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).executeWithOne());
assertEquals(7, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).executeWithASingleOne(1, 2, 3));
}
public interface ReturnsVoid {
String[] ARGUMENTS = new String[] {"map"};
void execute(Map<String, Object> map);
}
public void testReturnsVoid() {
Map<String, Object> map = new HashMap<>();
scriptEngine.compile(ReturnsVoid.class, null, "map.a = 'foo'", emptyMap()).execute(map);
assertEquals(singletonMap("a", "foo"), map);
scriptEngine.compile(ReturnsVoid.class, null, "map.remove('a')", emptyMap()).execute(map);
assertEquals(emptyMap(), map);
String debug = Debugger.toString(ReturnsVoid.class, "int i = 0", new CompilerSettings());
// The important thing is that this contains the opcode for returning void
assertThat(debug, containsString(" RETURN"));
// We shouldn't contain any weird "default to null" logic
assertThat(debug, not(containsString("ACONST_NULL")));
}
public interface ReturnsPrimitiveBoolean {
String[] ARGUMENTS = new String[] {};
boolean execute();
}
public void testReturnsPrimitiveBoolean() {
assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "true", emptyMap()).execute());
assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "false", emptyMap()).execute());
assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "Boolean.TRUE", emptyMap()).execute());
assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "Boolean.FALSE", emptyMap()).execute());
assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = true; i", emptyMap()).execute());
assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = Boolean.TRUE; i", emptyMap()).execute());
assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "true || false", emptyMap()).execute());
String debug = Debugger.toString(ReturnsPrimitiveBoolean.class, "false", new CompilerSettings());
assertThat(debug, containsString("ICONST_0"));
// The important thing here is that we have the bytecode for returning an integer instead of an object. booleans are integers.
assertThat(debug, containsString("IRETURN"));
Exception e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "1L", emptyMap()).execute());
assertEquals("Cannot cast from [long] to [boolean].", e.getMessage());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "1.1f", emptyMap()).execute());
assertEquals("Cannot cast from [float] to [boolean].", e.getMessage());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "1.1d", emptyMap()).execute());
assertEquals("Cannot cast from [double] to [boolean].", e.getMessage());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1L; i", emptyMap()).execute());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1.1f; i", emptyMap()).execute());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1.1d; i", emptyMap()).execute());
assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "int i = 0", emptyMap()).execute());
}
public interface ReturnsPrimitiveInt {
String[] ARGUMENTS = new String[] {};
int execute();
}
public void testReturnsPrimitiveInt() {
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1L", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1.1d", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1.1f", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "Integer.valueOf(1)", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1; i", emptyMap()).execute());
assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = Integer.valueOf(1); i", emptyMap()).execute());
assertEquals(2, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1 + 1", emptyMap()).execute());
String debug = Debugger.toString(ReturnsPrimitiveInt.class, "1", new CompilerSettings());
assertThat(debug, containsString("ICONST_1"));
// The important thing here is that we have the bytecode for returning an integer instead of an object
assertThat(debug, containsString("IRETURN"));
Exception e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1L", emptyMap()).execute());
assertEquals("Cannot cast from [long] to [int].", e.getMessage());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1.1f", emptyMap()).execute());
assertEquals("Cannot cast from [float] to [int].", e.getMessage());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1.1d", emptyMap()).execute());
assertEquals("Cannot cast from [double] to [int].", e.getMessage());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1L; i", emptyMap()).execute());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1.1f; i", emptyMap()).execute());
expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1.1d; i", emptyMap()).execute());
assertEquals(0, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "int i = 0", emptyMap()).execute());
}
public interface ReturnsPrimitiveFloat {
String[] ARGUMENTS = new String[] {};
float execute();
}
public void testReturnsPrimitiveFloat() {
assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "1.1f", emptyMap()).execute(), 0);
assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "(float) 1.1d", emptyMap()).execute(), 0);
assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = 1.1f; d", emptyMap()).execute(), 0);
assertEquals(1.1f,
scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = Float.valueOf(1.1f); d", emptyMap()).execute(), 0);
assertEquals(1.1f + 6.7f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "1.1f + 6.7f", emptyMap()).execute(), 0);
Exception e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "1.1d", emptyMap()).execute());
assertEquals("Cannot cast from [double] to [float].", e.getMessage());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = 1.1d; d", emptyMap()).execute());
e = expectScriptThrows(ClassCastException.class, () ->
scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = Double.valueOf(1.1); d", emptyMap()).execute());
String debug = Debugger.toString(ReturnsPrimitiveFloat.class, "1f", new CompilerSettings());
assertThat(debug, containsString("FCONST_1"));
// The important thing here is that we have the bytecode for returning a float instead of an object
assertThat(debug, containsString("FRETURN"));
assertEquals(0.0f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "int i = 0", emptyMap()).execute(), 0);
}
public interface ReturnsPrimitiveDouble {
String[] ARGUMENTS = new String[] {};
double execute();
}
public void testReturnsPrimitiveDouble() {
assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1", emptyMap()).execute(), 0);
assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1L", emptyMap()).execute(), 0);
assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1d", emptyMap()).execute(), 0);
assertEquals((double) 1.1f, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1f", emptyMap()).execute(), 0);
assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "Double.valueOf(1.1)", emptyMap()).execute(), 0);
assertEquals((double) 1.1f,
scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "Float.valueOf(1.1f)", emptyMap()).execute(), 0);
assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1; d", emptyMap()).execute(), 0);
assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1L; d", emptyMap()).execute(), 0);
assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1.1d; d", emptyMap()).execute(), 0);
assertEquals((double) 1.1f, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1.1f; d", emptyMap()).execute(), 0);
assertEquals(1.1,
scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = Double.valueOf(1.1); d", emptyMap()).execute(), 0);
assertEquals((double) 1.1f,
scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = Float.valueOf(1.1f); d", emptyMap()).execute(), 0);
assertEquals(1.1 + 6.7, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1 + 6.7", emptyMap()).execute(), 0);
String debug = Debugger.toString(ReturnsPrimitiveDouble.class, "1", new CompilerSettings());
assertThat(debug, containsString("DCONST_1"));
// The important thing here is that we have the bytecode for returning a double instead of an object
assertThat(debug, containsString("DRETURN"));
assertEquals(0.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "int i = 0", emptyMap()).execute(), 0);
}
public interface NoArgumentsConstant {
Object execute(String foo);
}
public void testNoArgumentsConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(NoArgumentsConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
+ "names of the method arguments but [" + NoArgumentsConstant.class.getName() + "] doesn't have one."));
}
public interface WrongArgumentsConstant {
boolean[] ARGUMENTS = new boolean[] {false};
Object execute(String foo);
}
public void testWrongArgumentsConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(WrongArgumentsConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
+ "names of the method arguments but [" + WrongArgumentsConstant.class.getName() + "] doesn't have one."));
}
public interface WrongLengthOfArgumentConstant {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo);
}
public void testWrongLengthOfArgumentConstant() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(WrongLengthOfArgumentConstant.class, null, "1", emptyMap()));
assertThat(e.getMessage(), startsWith("[" + WrongLengthOfArgumentConstant.class.getName() + "#ARGUMENTS] has length [2] but ["
+ WrongLengthOfArgumentConstant.class.getName() + "#execute] takes [1] argument."));
}
public interface UnknownArgType {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgType foo);
}
public void testUnknownArgType() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(UnknownArgType.class, null, "1", emptyMap()));
assertEquals("[foo] is of unknown type [" + UnknownArgType.class.getName() + ". Painless interfaces can only accept arguments "
+ "that are of whitelisted types.", e.getMessage());
}
public interface UnknownReturnType {
String[] ARGUMENTS = new String[] {"foo"};
UnknownReturnType execute(String foo);
}
public void testUnknownReturnType() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(UnknownReturnType.class, null, "1", emptyMap()));
assertEquals("Painless can only implement execute methods returning a whitelisted type but [" + UnknownReturnType.class.getName()
+ "#execute] returns [" + UnknownReturnType.class.getName() + "] which isn't whitelisted.", e.getMessage());
}
public interface UnknownArgTypeInArray {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(UnknownArgTypeInArray[] foo);
}
public void testUnknownArgTypeInArray() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(UnknownArgTypeInArray.class, null, "1", emptyMap()));
assertEquals("[foo] is of unknown type [" + UnknownArgTypeInArray.class.getName() + ". Painless interfaces can only accept "
+ "arguments that are of whitelisted types.", e.getMessage());
}
public interface TwoExecuteMethods {
Object execute();
Object execute(boolean foo);
}
public void testTwoExecuteMethods() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(TwoExecuteMethods.class, null, "null", emptyMap()));
assertEquals("Painless can only implement interfaces that have a single method named [execute] but ["
+ TwoExecuteMethods.class.getName() + "] has more than one.", e.getMessage());
}
public interface BadMethod {
Object something();
}
public void testBadMethod() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(BadMethod.class, null, "null", emptyMap()));
assertEquals("Painless can only implement methods named [execute] and [uses$argName] but [" + BadMethod.class.getName()
+ "] contains a method named [something]", e.getMessage());
}
public interface BadUsesReturn {
String[] ARGUMENTS = new String[] {"foo"};
Object execute(String foo);
Object uses$foo();
}
public void testBadUsesReturn() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(BadUsesReturn.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that return boolean but [" + BadUsesReturn.class.getName()
+ "#uses$foo] returns [java.lang.Object].", e.getMessage());
}
public interface BadUsesParameter {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo, String bar);
boolean uses$bar(boolean foo);
}
public void testBadUsesParameter() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(BadUsesParameter.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that do not take parameters but [" + BadUsesParameter.class.getName()
+ "#uses$bar] does.", e.getMessage());
}
public interface BadUsesName {
String[] ARGUMENTS = new String[] {"foo", "bar"};
Object execute(String foo, String bar);
boolean uses$baz();
}
public void testBadUsesName() {
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
scriptEngine.compile(BadUsesName.class, null, "null", emptyMap()));
assertEquals("Painless can only implement uses$ methods that match a parameter name but [" + BadUsesName.class.getName()
+ "#uses$baz] doesn't match any of [foo, bar].", e.getMessage());
}
}