/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.painless;
import junit.framework.AssertionFailedError;
import org.opensearch.common.settings.Settings;
import org.opensearch.painless.antlr.Walker;
import org.opensearch.painless.spi.Allowlist;
import org.opensearch.painless.spi.AllowlistLoader;
import org.opensearch.script.ScriptContext;
import org.opensearch.script.ScriptException;
import org.opensearch.test.OpenSearchTestCase;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.hasSize;
import static org.opensearch.painless.action.PainlessExecuteAction.PainlessTestScript;
/**
* Base test case for scripting unit tests.
*
* Typically just asserts the output of {@code exec()}
*/
public abstract class ScriptTestCase extends OpenSearchTestCase {
private static final PainlessScriptEngine SCRIPT_ENGINE = new PainlessScriptEngine(Settings.EMPTY, newDefaultContexts());
/** Creates a new contexts map with PainlessTextScript = org.opensearch.painless.test */
protected static Map, List> newDefaultContexts() {
Map, List> contexts = new HashMap<>();
List allowlists = new ArrayList<>(Allowlist.BASE_ALLOWLISTS);
allowlists.add(AllowlistLoader.loadFromResourceFiles(Allowlist.class, "org.opensearch.painless.test"));
contexts.put(PainlessTestScript.CONTEXT, allowlists);
return contexts;
}
/**
* Get the script engine to use.
* If you override this method in order to customize the settings, it is recommended
* to setup/teardown a class-level fixture and return it directly for performance.
*/
protected PainlessScriptEngine getEngine() {
return SCRIPT_ENGINE;
}
/**
* Uses the {@link Debugger} to get the bytecode output for a script and compare
* it against an expected bytecode passed in as a String.
*/
public static final void assertBytecodeExists(String script, String bytecode) {
final String asm = Debugger.toString(script);
assertTrue("bytecode not found, got: \n" + asm, asm.contains(bytecode));
}
/**
* Uses the {@link Debugger} to get the bytecode output for a script and compare
* it against an expected bytecode pattern as a regular expression (please try to avoid!)
*/
public static final void assertBytecodeHasPattern(String script, String pattern) {
final String asm = Debugger.toString(script);
assertTrue("bytecode not found, got: \n" + asm, asm.matches(pattern));
}
/** Checks a specific exception class is thrown (boxed inside ScriptException) and returns it. */
public static final T expectScriptThrows(Class expectedType, ThrowingRunnable runnable) {
return expectScriptThrows(expectedType, true, runnable);
}
/** Checks a specific exception class is thrown (boxed inside ScriptException) and returns it. */
public static final T expectScriptThrows(
Class expectedType,
boolean shouldHaveScriptStack,
ThrowingRunnable runnable
) {
try {
runnable.run();
} catch (Throwable e) {
if (e instanceof ScriptException) {
boolean hasEmptyScriptStack = ((ScriptException) e).getScriptStack().isEmpty();
if (shouldHaveScriptStack && hasEmptyScriptStack) {
/* If this fails you *might* be missing -XX:-OmitStackTraceInFastThrow in the test jvm
* In Eclipse you can add this by default by going to Preference->Java->Installed JREs,
* clicking on the default JRE, clicking edit, and adding the flag to the
* "Default VM Arguments". */
AssertionFailedError assertion = new AssertionFailedError("ScriptException should have a scriptStack");
assertion.initCause(e);
throw assertion;
} else if (false == shouldHaveScriptStack && false == hasEmptyScriptStack) {
AssertionFailedError assertion = new AssertionFailedError("ScriptException shouldn't have a scriptStack");
assertion.initCause(e);
throw assertion;
}
e = e.getCause();
if (expectedType.isInstance(e)) {
return expectedType.cast(e);
}
} else {
AssertionFailedError assertion = new AssertionFailedError("Expected boxed ScriptException");
assertion.initCause(e);
throw assertion;
}
AssertionFailedError assertion = new AssertionFailedError(
"Unexpected exception type, expected " + expectedType.getSimpleName()
);
assertion.initCause(e);
throw assertion;
}
throw new AssertionFailedError("Expected exception " + expectedType.getSimpleName());
}
/**
* Asserts that the script_stack looks right.
*/
public static final void assertScriptStack(ScriptException e, String... stack) {
// This particular incantation of assertions makes the error messages more useful
try {
assertThat(e.getScriptStack(), hasSize(stack.length));
for (int i = 0; i < stack.length; i++) {
assertEquals(stack[i], e.getScriptStack().get(i));
}
} catch (AssertionError assertion) {
assertion.initCause(e);
throw assertion;
}
}
/** Compiles and returns the result of {@code script} */
public final Object exec(String script) {
return exec(script, null, true);
}
/** Compiles and returns the result of {@code script} with access to {@code picky} */
public final Object exec(String script, boolean picky) {
return exec(script, null, picky);
}
/** Compiles and returns the result of {@code script} with access to {@code vars} */
public final Object exec(String script, Map vars, boolean picky) {
Map compilerSettings = new HashMap<>();
compilerSettings.put(CompilerSettings.INITIAL_CALL_SITE_DEPTH, random().nextBoolean() ? "0" : "10");
return exec(script, vars, compilerSettings, picky);
}
/** Compiles and returns the result of {@code script} with access to {@code vars} and compile-time parameters */
public final Object exec(String script, Map vars, Map compileParams, boolean picky) {
// test for ambiguity errors before running the actual script if picky is true
if (picky) {
CompilerSettings pickySettings = new CompilerSettings();
pickySettings.setPicky(true);
pickySettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(Settings.EMPTY));
Walker.buildPainlessTree(getTestName(), script, pickySettings);
}
// test actual script execution
PainlessTestScript.Factory factory = getEngine().compile(null, script, PainlessTestScript.CONTEXT, compileParams);
PainlessTestScript testScript = factory.newInstance(vars == null ? Collections.emptyMap() : vars);
return testScript.execute();
}
}