/*
* 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 junit.framework.AssertionFailedError;
import org.apache.lucene.util.Constants;
import org.elasticsearch.script.ScriptException;
import java.lang.invoke.WrongMethodTypeException;
import java.util.Arrays;
import java.util.Collections;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.instanceOf;
public class WhenThingsGoWrongTests extends ScriptTestCase {
public void testNullPointer() {
expectScriptThrows(NullPointerException.class, () -> {
exec("int x = params['missing']; return x;");
});
expectScriptThrows(NullPointerException.class, () -> {
exec("Double.parseDouble(params['missing'])");
});
}
/**
* Test that the scriptStack looks good. By implication this tests that we build proper "line numbers" in stack trace. These line
* numbers are really 1 based character numbers.
*/
public void testScriptStack() {
for (String type : new String[] {"String", "def "}) {
// trigger NPE at line 1 of the script
ScriptException exception = expectThrows(ScriptException.class, () -> {
exec(type + " x = null; boolean y = x.isEmpty();\n" +
"return y;");
});
// null deref at x.isEmpty(), the '.' is offset 30
assertScriptElementColumn(30, exception);
assertScriptStack(exception,
"y = x.isEmpty();\n",
" ^---- HERE");
assertThat(exception.getCause(), instanceOf(NullPointerException.class));
// trigger NPE at line 2 of the script
exception = expectThrows(ScriptException.class, () -> {
exec(type + " x = null;\n" +
"return x.isEmpty();");
});
// null deref at x.isEmpty(), the '.' is offset 25
assertScriptElementColumn(25, exception);
assertScriptStack(exception,
"return x.isEmpty();",
" ^---- HERE");
assertThat(exception.getCause(), instanceOf(NullPointerException.class));
// trigger NPE at line 3 of the script
exception = expectThrows(ScriptException.class, () -> {
exec(type + " x = null;\n" +
type + " y = x;\n" +
"return y.isEmpty();");
});
// null deref at y.isEmpty(), the '.' is offset 39
assertScriptElementColumn(39, exception);
assertScriptStack(exception,
"return y.isEmpty();",
" ^---- HERE");
assertThat(exception.getCause(), instanceOf(NullPointerException.class));
// trigger NPE at line 4 in script (inside conditional)
exception = expectThrows(ScriptException.class, () -> {
exec(type + " x = null;\n" +
"boolean y = false;\n" +
"if (!y) {\n" +
" y = x.isEmpty();\n" +
"}\n" +
"return y;");
});
// null deref at x.isEmpty(), the '.' is offset 53
assertScriptElementColumn(53, exception);
assertScriptStack(exception,
"y = x.isEmpty();\n}\n",
" ^---- HERE");
assertThat(exception.getCause(), instanceOf(NullPointerException.class));
}
}
private void assertScriptElementColumn(int expectedColumn, ScriptException exception) {
StackTraceElement[] stackTrace = exception.getCause().getStackTrace();
for (int i = 0; i < stackTrace.length; i++) {
if (WriterConstants.CLASS_NAME.equals(stackTrace[i].getClassName())) {
if (expectedColumn + 1 != stackTrace[i].getLineNumber()) {
AssertionFailedError assertion = new AssertionFailedError("Expected column to be [" + expectedColumn + "] but was ["
+ stackTrace[i].getLineNumber() + "]");
assertion.initCause(exception);
throw assertion;
}
return;
}
}
fail("didn't find script stack element");
}
public void testInvalidShift() {
expectScriptThrows(ClassCastException.class, () -> {
exec("float x = 15F; x <<= 2; return x;");
});
expectScriptThrows(ClassCastException.class, () -> {
exec("double x = 15F; x <<= 2; return x;");
});
}
public void testBogusParameter() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
exec("return 5;", null, Collections.singletonMap("bogusParameterKey", "bogusParameterValue"), null, true);
});
assertTrue(expected.getMessage().contains("Unrecognized compile-time parameter"));
}
public void testInfiniteLoops() {
PainlessError expected = expectScriptThrows(PainlessError.class, () -> {
exec("boolean x = true; while (x) {}");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("while (true) {int y = 5;}");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("while (true) { boolean x = true; while (x) {} }");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("while (true) { boolean x = false; while (x) {} }");
fail("should have hit PainlessError");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("boolean x = true; for (;x;) {}");
fail("should have hit PainlessError");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("for (;;) {int x = 5;}");
fail("should have hit PainlessError");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
expected = expectScriptThrows(PainlessError.class, () -> {
exec("def x = true; do {int y = 5;} while (x)");
fail("should have hit PainlessError");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
RuntimeException parseException = expectScriptThrows(RuntimeException.class, () -> {
exec("try { int x; } catch (PainlessError error) {}", false);
fail("should have hit ParseException");
});
assertTrue(parseException.getMessage().contains("unexpected token ['PainlessError']"));
}
public void testLoopLimits() {
// right below limit: ok
exec("for (int x = 0; x < 999999; ++x) {}");
PainlessError expected = expectScriptThrows(PainlessError.class, () -> {
exec("for (int x = 0; x < 1000000; ++x) {}");
});
assertTrue(expected.getMessage().contains(
"The maximum number of statements that can be executed in a loop has been reached."));
}
public void testSourceLimits() {
final char[] tooManyChars = new char[Compiler.MAXIMUM_SOURCE_LENGTH + 1];
Arrays.fill(tooManyChars, '0');
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, false, () -> {
exec(new String(tooManyChars));
});
assertTrue(expected.getMessage().contains("Scripts may be no longer than"));
final char[] exactlyAtLimit = new char[Compiler.MAXIMUM_SOURCE_LENGTH];
Arrays.fill(exactlyAtLimit, '0');
// ok
assertEquals(0, exec(new String(exactlyAtLimit)));
}
public void testIllegalDynamicMethod() {
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, () -> {
exec("def x = 'test'; return x.getClass().toString()");
});
assertTrue(expected.getMessage().contains("Unable to find dynamic method"));
}
public void testDynamicNPE() {
expectScriptThrows(NullPointerException.class, () -> {
exec("def x = null; return x.toString()");
});
}
public void testDynamicWrongArgs() {
expectScriptThrows(WrongMethodTypeException.class, () -> {
exec("def x = new ArrayList(); return x.get('bogus');");
});
}
public void testDynamicArrayWrongIndex() {
expectScriptThrows(WrongMethodTypeException.class, () -> {
exec("def x = new long[1]; x[0]=1; return x['bogus'];");
});
}
public void testDynamicListWrongIndex() {
expectScriptThrows(WrongMethodTypeException.class, () -> {
exec("def x = new ArrayList(); x.add('foo'); return x['bogus'];");
});
}
/**
* Makes sure that we present a useful error message with a misplaced right-curly. This is important because we do some funky things in
* the parser with right-curly brackets to allow statements to be delimited by them at the end of blocks.
*/
public void testRCurlyNotDelim() {
IllegalArgumentException e = expectScriptThrows(IllegalArgumentException.class, () -> {
// We don't want PICKY here so we get the normal error message
exec("def i = 1} return 1", emptyMap(), emptyMap(), null, false);
});
assertEquals("unexpected token ['}'] was expecting one of [<EOF>].", e.getMessage());
}
public void testBadBoxingCast() {
expectScriptThrows(ClassCastException.class, () -> {
exec("BitSet bs = new BitSet(); bs.and(2);");
});
}
public void testOutOfMemoryError() {
assumeTrue("test only happens to work for sure on oracle jre", Constants.JAVA_VENDOR.startsWith("Oracle"));
expectScriptThrows(OutOfMemoryError.class, () -> {
exec("int[] x = new int[Integer.MAX_VALUE - 1];");
});
}
public void testStackOverflowError() {
expectScriptThrows(StackOverflowError.class, () -> {
exec("void recurse(int x, int y) {recurse(x, y)} recurse(1, 2);");
});
}
public void testRegexDisabledByDefault() {
IllegalStateException e = expectThrows(IllegalStateException.class, () -> exec("return 'foo' ==~ /foo/"));
assertEquals("Regexes are disabled. Set [script.painless.regex.enabled] to [true] in elasticsearch.yaml to allow them. "
+ "Be careful though, regexes break out of Painless's protection against deep recursion and long loops.", e.getMessage());
}
public void testCanNotOverrideRegexEnabled() {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> exec("", null, singletonMap(CompilerSettings.REGEX_ENABLED.getKey(), "true"), null, false));
assertEquals("[painless.regex.enabled] can only be set on node startup.", e.getMessage());
}
public void testInvalidIntConstantSuggestsLong() {
IllegalArgumentException e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return 864000000000"));
assertEquals("Invalid int constant [864000000000]. If you want a long constant then change it to [864000000000L].", e.getMessage());
assertEquals(864000000000L, exec("return 864000000000L"));
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return -864000000000"));
assertEquals("Invalid int constant [-864000000000]. If you want a long constant then change it to [-864000000000L].",
e.getMessage());
assertEquals(-864000000000L, exec("return -864000000000L"));
// If it isn't a valid long we don't give any suggestions
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return 92233720368547758070"));
assertEquals("Invalid int constant [92233720368547758070].", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return -92233720368547758070"));
assertEquals("Invalid int constant [-92233720368547758070].", e.getMessage());
}
public void testQuestionSpaceDotIsNotNullSafeDereference() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec("return params.a? .b", false));
assertEquals("invalid sequence of tokens near ['.'].", e.getMessage());
}
public void testBadStringEscape() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec("'\\a'", false));
assertEquals("unexpected character ['\\a]. The only valid escape sequences in strings starting with ['] are [\\\\] and [\\'].",
e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("\"\\a\"", false));
assertEquals("unexpected character [\"\\a]. The only valid escape sequences in strings starting with [\"] are [\\\\] and [\\\"].",
e.getMessage());
}
public void testRegularUnexpectedCharacter() {
Exception e = expectScriptThrows(IllegalArgumentException.class, () -> exec("'", false));
assertEquals("unexpected character ['].", e.getMessage());
e = expectScriptThrows(IllegalArgumentException.class, () -> exec("'cat", false));
assertEquals("unexpected character ['cat].", e.getMessage());
}
}