/*
* 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 static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
/**
* Tests for the Elvis operator ({@code ?:}).
*/
public class ElvisTests extends ScriptTestCase {
public void testBasics() {
// Basics
assertEquals("str", exec("return params.a ?: 'str'"));
assertEquals("str", exec("return params.a ?: 'str2'", singletonMap("a", "str"), true));
assertEquals("str", exec("return params.a ?: 'asdf'", singletonMap("a", "str"), true));
// Assigning to a primitive
assertCannotReturnPrimitive("int i = params.a ?: 1; return i");
assertCannotReturnPrimitive("Integer a = Integer.valueOf(1); int b = a ?: 2; return b");
assertCannotReturnPrimitive("Integer a = Integer.valueOf(1); int b = a ?: Integer.valueOf(2); return b");
assertEquals(2, exec("int i = (params.a ?: Integer.valueOf(2)).intValue(); return i"));
assertEquals(1, exec("int i = (params.a ?: Integer.valueOf(2)).intValue(); return i", singletonMap("a", 1), true));
assertEquals(1, exec("Integer a = Integer.valueOf(1); int b = (a ?: Integer.valueOf(2)).intValue(); return b"));
assertEquals(2, exec("Integer a = null; int b = (a ?: Integer.valueOf(2)).intValue(); return b"));
// Assigning to an object
assertEquals(1, exec("Integer i = params.a ?: Integer.valueOf(1); return i"));
assertEquals(1, exec("Integer i = params.a ?: Integer.valueOf(2); return i", singletonMap("a", 1), true));
assertEquals(1, exec("Integer a = Integer.valueOf(1); Integer b = a ?: Integer.valueOf(2); return b"));
assertEquals(2, exec("Integer a = null; Integer b = a ?: Integer.valueOf(2); return b"));
// Explicit casting
assertEquals(1, exec("return (Integer)(params.a ?: Integer.valueOf(1))"));
assertEquals(1, exec("return (Integer)(params.a ?: Integer.valueOf(2))", singletonMap("a", 1), true));
assertCannotReturnPrimitive("return (int)(params.a ?: 1)");
// Now some chains
assertEquals(1, exec("return params.a ?: params.a ?: 1"));
assertEquals(1, exec("return params.a ?: params.b ?: 'j'", singletonMap("b", 1), true));
assertEquals(1, exec("return params.a ?: params.b ?: 'j'", singletonMap("a", 1), true));
// Precedence
assertEquals(1, exec("return params.a ?: 2 + 2", singletonMap("a", 1), true));
assertEquals(4, exec("return params.a ?: 2 + 2"));
assertEquals(2, exec("return params.a + 1 ?: 2 + 2", singletonMap("a", 1), true)); // Yes, this is silly, but it should be valid
// Weird casts
assertEquals(1, exec("int i = params.i; String s = params.s; return s ?: i", singletonMap("i", 1), true));
assertEquals("str", exec("Integer i = params.i; String s = params.s; return s ?: i", singletonMap("s", "str"), true));
// Combining
assertEquals(2, exec("return (params.a ?: 0) + 1", singletonMap("a", 1), true));
assertEquals(1, exec("return (params.a ?: 0) + 1"));
assertEquals(2, exec("return (params.a ?: ['b': 10]).b + 1", singletonMap("a", singletonMap("b", 1)), true));
assertEquals(11, exec("return (params.a ?: ['b': 10]).b + 1"));
}
public void testWithNullSafeDereferences() {
assertEquals(1, exec("return params.a?.b ?: 1"));
assertEquals(1, exec("return params.a?.b ?: 2", singletonMap("a", singletonMap("b", 1)), true));
// TODO This could be expanded to allow primitives where neither of the two operations allow them alone
}
public void testLazy() {
assertEquals(1, exec("def fail() {throw new RuntimeException('test')} return params.a ?: fail()", singletonMap("a", 1), true));
Exception e = expectScriptThrows(RuntimeException.class, () ->
exec("def fail() {throw new RuntimeException('test')} return params.a ?: fail()"));
assertEquals(e.getMessage(), "test");
}
/**
* Checks that {@code a ?: b ?: c} is be parsed as {@code a ?: (b ?: c)} instead of {@code (a ?: b) ?: c} which is nice because the
* first one only needs one comparison if the {@code a} is non-null while the second one needs two.
*/
public void testRightAssociative() {
checkOneBranch("params.a ?: (params.b ?: params.c)", true);
checkOneBranch("(params.a ?: params.b) ?: params.c", false);
checkOneBranch("params.a ?: params.b ?: params.c", true);
}
private void checkOneBranch(String code, boolean expectOneBranch) {
/* Sadly this is a super finicky about the output of the disassembly but I think it is worth having because it makes sure that
* the code generated for the elvis operator is as efficient as possible. */
String disassembled = Debugger.toString(code);
int firstLookup = disassembled.indexOf("INVOKEINTERFACE java/util/Map.get (Ljava/lang/Object;)Ljava/lang/Object;");
assertThat(disassembled, firstLookup, greaterThan(-1));
int firstElvisDestinationLabelIndex = disassembled.indexOf("IFNONNULL L", firstLookup);
assertThat(disassembled, firstElvisDestinationLabelIndex, greaterThan(-1));
String firstElvisDestinationLabel = disassembled.substring(firstElvisDestinationLabelIndex + "IFNONNULL ".length(),
disassembled.indexOf('\n', firstElvisDestinationLabelIndex));
int firstElvisDestionation = disassembled.indexOf(" " + firstElvisDestinationLabel);
assertThat(disassembled, firstElvisDestionation, greaterThan(-1));
int ifAfterFirstElvisDestination = disassembled.indexOf("IF", firstElvisDestionation);
if (expectOneBranch) {
assertThat(disassembled, ifAfterFirstElvisDestination, lessThan(0));
} else {
assertThat(disassembled, ifAfterFirstElvisDestination, greaterThan(-1));
}
int returnAfterFirstElvisDestination = disassembled.indexOf("RETURN", firstElvisDestionation);
assertThat(disassembled, returnAfterFirstElvisDestination, greaterThan(-1));
}
public void testExtraneous() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec("int i = params.a; return i ?: 1"));
assertEquals("Extraneous elvis operator. LHS is a primitive.", e.getMessage());
expectScriptThrows(IllegalArgumentException.class, () -> exec("int i = params.a; return i + 10 ?: 'ignored'"));
assertEquals("Extraneous elvis operator. LHS is a primitive.", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return 'cat' ?: 1"));
assertEquals("Extraneous elvis operator. LHS is a constant.", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return null ?: 'j'"));
assertEquals("Extraneous elvis operator. LHS is null.", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return params.a ?: null ?: 'j'"));
assertEquals("Extraneous elvis operator. LHS is null.", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return params.a ?: null"));
assertEquals("Extraneous elvis operator. RHS is null.", e.getMessage());
}
public void testQuestionSpaceColonIsNotElvis() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return params.a ? : 1", false));
assertEquals("invalid sequence of tokens near [':'].", e.getMessage());
}
private void assertCannotReturnPrimitive(String script) {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec(script));
assertEquals("Evlis operator cannot return primitives", e.getMessage());
}
}