Skip to content

Commit db0a5e4

Browse files
authored
Painless: more testing for script_stack (#24168)
`script_stack` is super useful when debugging Painless scripts because it skips all the "weird" stuff involved that obfuscates where the actual error is. It skips Painless's internals and call site bootstrapping. It works fine, but it didn't have many tests. This converts a test that we had for line numbers into a test for the `script_stack`. The line numbers test was an indirect test for `script_stack`.
1 parent 8f666a7 commit db0a5e4

File tree

9 files changed

+144
-63
lines changed

9 files changed

+144
-63
lines changed

modules/lang-painless/src/main/java/org/elasticsearch/painless/CompilerSettings.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public final class CompilerSettings {
4343
public static final String PICKY = "picky";
4444

4545
/**
46-
* For testing: do not use.
46+
* Hack to set the initial "depth" for the {@link DefBootstrap.PIC} and {@link DefBootstrap.MIC}. Only used for testing: do not
47+
* overwrite.
4748
*/
4849
public static final String INITIAL_CALL_SITE_DEPTH = "initialCallSiteDepth";
4950

modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayLikeObjectTestCase.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ private void arrayLoadStoreTestCase(boolean declareAsDef, String valueType, Obje
7777
}
7878

7979
private void expectOutOfBounds(int index, String script, Object val) {
80-
IndexOutOfBoundsException e = expectScriptThrows(IndexOutOfBoundsException.class,
81-
() -> exec(script, singletonMap("val", val), true));
80+
IndexOutOfBoundsException e = expectScriptThrows(IndexOutOfBoundsException.class, () ->
81+
exec(script, singletonMap("val", val), true));
8282
try {
8383
assertThat(e.getMessage(), outOfBoundsExceptionMessageMatcher(index, 5));
8484
} catch (AssertionError ae) {

modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicExpressionTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public void testNullSafeDeref() {
186186
assertNull( exec("def a = null; return a?.toString()"));
187187
assertEquals("foo", exec("def a = 'foo'; return a?.toString()"));
188188
// Call with primitive result
189-
assertMustBeNullable( "String a = null; return a?.length()");
189+
assertMustBeNullable( "String a = null; return a?.length()");
190190
assertMustBeNullable( "String a = 'foo'; return a?.length()");
191191
assertNull( exec("def a = null; return a?.length()"));
192192
assertEquals(3, exec("def a = 'foo'; return a?.length()"));
@@ -265,7 +265,7 @@ public void testNullSafeDeref() {
265265
}
266266

267267
private void assertMustBeNullable(String script) {
268-
Exception e = expectScriptThrows(IllegalArgumentException.class , () -> exec(script));
268+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () -> exec(script));
269269
assertEquals("Result of null safe operator must be nullable", e.getMessage());
270270
}
271271
}

modules/lang-painless/src/test/java/org/elasticsearch/painless/ImplementInterfacesTests.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ public interface NoArgumentsConstant {
325325
Object execute(String foo);
326326
}
327327
public void testNoArgumentsConstant() {
328-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
328+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
329329
scriptEngine.compile(NoArgumentsConstant.class, null, "1", emptyMap()));
330330
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
331331
+ "names of the method arguments but [" + NoArgumentsConstant.class.getName() + "] doesn't have one."));
@@ -336,7 +336,7 @@ public interface WrongArgumentsConstant {
336336
Object execute(String foo);
337337
}
338338
public void testWrongArgumentsConstant() {
339-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
339+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
340340
scriptEngine.compile(WrongArgumentsConstant.class, null, "1", emptyMap()));
341341
assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
342342
+ "names of the method arguments but [" + WrongArgumentsConstant.class.getName() + "] doesn't have one."));
@@ -347,7 +347,7 @@ public interface WrongLengthOfArgumentConstant {
347347
Object execute(String foo);
348348
}
349349
public void testWrongLengthOfArgumentConstant() {
350-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
350+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
351351
scriptEngine.compile(WrongLengthOfArgumentConstant.class, null, "1", emptyMap()));
352352
assertThat(e.getMessage(), startsWith("[" + WrongLengthOfArgumentConstant.class.getName() + "#ARGUMENTS] has length [2] but ["
353353
+ WrongLengthOfArgumentConstant.class.getName() + "#execute] takes [1] argument."));
@@ -358,7 +358,7 @@ public interface UnknownArgType {
358358
Object execute(UnknownArgType foo);
359359
}
360360
public void testUnknownArgType() {
361-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
361+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
362362
scriptEngine.compile(UnknownArgType.class, null, "1", emptyMap()));
363363
assertEquals("[foo] is of unknown type [" + UnknownArgType.class.getName() + ". Painless interfaces can only accept arguments "
364364
+ "that are of whitelisted types.", e.getMessage());
@@ -369,7 +369,7 @@ public interface UnknownReturnType {
369369
UnknownReturnType execute(String foo);
370370
}
371371
public void testUnknownReturnType() {
372-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
372+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
373373
scriptEngine.compile(UnknownReturnType.class, null, "1", emptyMap()));
374374
assertEquals("Painless can only implement execute methods returning a whitelisted type but [" + UnknownReturnType.class.getName()
375375
+ "#execute] returns [" + UnknownReturnType.class.getName() + "] which isn't whitelisted.", e.getMessage());
@@ -380,7 +380,7 @@ public interface UnknownArgTypeInArray {
380380
Object execute(UnknownArgTypeInArray[] foo);
381381
}
382382
public void testUnknownArgTypeInArray() {
383-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
383+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
384384
scriptEngine.compile(UnknownArgTypeInArray.class, null, "1", emptyMap()));
385385
assertEquals("[foo] is of unknown type [" + UnknownArgTypeInArray.class.getName() + ". Painless interfaces can only accept "
386386
+ "arguments that are of whitelisted types.", e.getMessage());
@@ -391,7 +391,7 @@ public interface TwoExecuteMethods {
391391
Object execute(boolean foo);
392392
}
393393
public void testTwoExecuteMethods() {
394-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
394+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
395395
scriptEngine.compile(TwoExecuteMethods.class, null, "null", emptyMap()));
396396
assertEquals("Painless can only implement interfaces that have a single method named [execute] but ["
397397
+ TwoExecuteMethods.class.getName() + "] has more than one.", e.getMessage());
@@ -401,7 +401,7 @@ public interface BadMethod {
401401
Object something();
402402
}
403403
public void testBadMethod() {
404-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
404+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
405405
scriptEngine.compile(BadMethod.class, null, "null", emptyMap()));
406406
assertEquals("Painless can only implement methods named [execute] and [uses$argName] but [" + BadMethod.class.getName()
407407
+ "] contains a method named [something]", e.getMessage());
@@ -413,7 +413,7 @@ public interface BadUsesReturn {
413413
Object uses$foo();
414414
}
415415
public void testBadUsesReturn() {
416-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
416+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
417417
scriptEngine.compile(BadUsesReturn.class, null, "null", emptyMap()));
418418
assertEquals("Painless can only implement uses$ methods that return boolean but [" + BadUsesReturn.class.getName()
419419
+ "#uses$foo] returns [java.lang.Object].", e.getMessage());
@@ -425,7 +425,7 @@ public interface BadUsesParameter {
425425
boolean uses$bar(boolean foo);
426426
}
427427
public void testBadUsesParameter() {
428-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
428+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
429429
scriptEngine.compile(BadUsesParameter.class, null, "null", emptyMap()));
430430
assertEquals("Painless can only implement uses$ methods that do not take parameters but [" + BadUsesParameter.class.getName()
431431
+ "#uses$bar] does.", e.getMessage());
@@ -437,7 +437,7 @@ public interface BadUsesName {
437437
boolean uses$baz();
438438
}
439439
public void testBadUsesName() {
440-
Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
440+
Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
441441
scriptEngine.compile(BadUsesName.class, null, "null", emptyMap()));
442442
assertEquals("Painless can only implement uses$ methods that match a parameter name but [" + BadUsesName.class.getName()
443443
+ "#uses$baz] doesn't match any of [foo, bar].", e.getMessage());

modules/lang-painless/src/test/java/org/elasticsearch/painless/LambdaTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public void testNestedCaptureParams() {
204204

205205
public void testWrongArity() {
206206
assumeFalse("JDK is JDK 9", Constants.JRE_IS_MINIMUM_JAVA9);
207-
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, () -> {
207+
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, false, () -> {
208208
exec("Optional.empty().orElseGet(x -> x);");
209209
});
210210
assertTrue(expected.getMessage().contains("Incorrect number of parameters"));
@@ -220,7 +220,7 @@ public void testWrongArityDef() {
220220

221221
public void testWrongArityNotEnough() {
222222
assumeFalse("JDK is JDK 9", Constants.JRE_IS_MINIMUM_JAVA9);
223-
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, () -> {
223+
IllegalArgumentException expected = expectScriptThrows(IllegalArgumentException.class, false, () -> {
224224
exec("List l = new ArrayList(); l.add(1); l.add(1); "
225225
+ "return l.stream().mapToInt(() -> 5).sum();");
226226
});

modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@
2626
import java.util.Arrays;
2727
import java.util.HashSet;
2828
import java.util.regex.Pattern;
29-
import java.util.regex.PatternSyntaxException;
3029

3130
import static java.util.Collections.singletonMap;
32-
import static org.hamcrest.Matchers.containsString;
3331

3432
public class RegexTests extends ScriptTestCase {
3533
@Override
@@ -264,8 +262,9 @@ public void testBadRegexPattern() {
264262
assertEquals("Error compiling regex: Illegal Unicode escape sequence", e.getCause().getMessage());
265263

266264
// And make sure the location of the error points to the offset inside the pattern
267-
assertEquals("/\\ujjjj/", e.getScriptStack().get(0));
268-
assertEquals(" ^---- HERE", e.getScriptStack().get(1));
265+
assertScriptStack(e,
266+
"/\\ujjjj/",
267+
" ^---- HERE");
269268
}
270269

271270
public void testRegexAgainstNumber() {

modules/lang-painless/src/test/java/org/elasticsearch/painless/ScriptTestCase.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.HashMap;
3636
import java.util.Map;
3737

38+
import static org.hamcrest.Matchers.hasSize;
39+
3840
/**
3941
* Base test case for scripting unit tests.
4042
* <p>
@@ -114,10 +116,29 @@ public void assertBytecodeHasPattern(String script, String pattern) {
114116

115117
/** Checks a specific exception class is thrown (boxed inside ScriptException) and returns it. */
116118
public static <T extends Throwable> T expectScriptThrows(Class<T> expectedType, ThrowingRunnable runnable) {
119+
return expectScriptThrows(expectedType, true, runnable);
120+
}
121+
122+
/** Checks a specific exception class is thrown (boxed inside ScriptException) and returns it. */
123+
public static <T extends Throwable> T expectScriptThrows(Class<T> expectedType, boolean shouldHaveScriptStack,
124+
ThrowingRunnable runnable) {
117125
try {
118126
runnable.run();
119127
} catch (Throwable e) {
120128
if (e instanceof ScriptException) {
129+
boolean hasEmptyScriptStack = ((ScriptException) e).getScriptStack().isEmpty();
130+
if (shouldHaveScriptStack && hasEmptyScriptStack) {
131+
if (0 != e.getCause().getStackTrace().length) {
132+
// Without -XX:-OmitStackTraceInFastThrow the jvm can eat the stack trace which causes us to ignore script_stack
133+
AssertionFailedError assertion = new AssertionFailedError("ScriptException should have a scriptStack");
134+
assertion.initCause(e);
135+
throw assertion;
136+
}
137+
} else if (false == shouldHaveScriptStack && false == hasEmptyScriptStack) {
138+
AssertionFailedError assertion = new AssertionFailedError("ScriptException shouldn't have a scriptStack");
139+
assertion.initCause(e);
140+
throw assertion;
141+
}
121142
e = e.getCause();
122143
if (expectedType.isInstance(e)) {
123144
return expectedType.cast(e);
@@ -134,4 +155,21 @@ public static <T extends Throwable> T expectScriptThrows(Class<T> expectedType,
134155
}
135156
throw new AssertionFailedError("Expected exception " + expectedType.getSimpleName());
136157
}
158+
159+
/**
160+
* Asserts that the script_stack looks right.
161+
*/
162+
public static void assertScriptStack(ScriptException e, String... stack) {
163+
// This particular incantation of assertions makes the error messages more useful
164+
try {
165+
assertThat(e.getScriptStack(), hasSize(stack.length));
166+
for (int i = 0; i < stack.length; i++) {
167+
assertEquals(stack[i], e.getScriptStack().get(i));
168+
}
169+
} catch (AssertionError assertion) {
170+
assertion.initCause(e);
171+
throw assertion;
172+
}
173+
}
174+
137175
}

modules/lang-painless/src/test/java/org/elasticsearch/painless/StringTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,12 @@ public void testStringAndCharacter() {
165165
assertEquals('c', exec("String s = \"c\"; (char)s"));
166166
assertEquals('c', exec("String s = 'c'; (char)s"));
167167

168-
ClassCastException expected = expectScriptThrows(ClassCastException.class, () -> {
168+
ClassCastException expected = expectScriptThrows(ClassCastException.class, false, () -> {
169169
assertEquals("cc", exec("return (String)(char)\"cc\""));
170170
});
171171
assertTrue(expected.getMessage().contains("Cannot cast [String] with length greater than one to [char]."));
172172

173-
expected = expectScriptThrows(ClassCastException.class, () -> {
173+
expected = expectScriptThrows(ClassCastException.class, false, () -> {
174174
assertEquals("cc", exec("return (String)(char)'cc'"));
175175
});
176176
assertTrue(expected.getMessage().contains("Cannot cast [String] with length greater than one to [char]."));

0 commit comments

Comments
 (0)