CommonsCompilerTestSuite.java

/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package util;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.codehaus.commons.compiler.AbstractJavaSourceClassLoader;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.ErrorHandler;
import org.codehaus.commons.compiler.IClassBodyEvaluator;
import org.codehaus.commons.compiler.ICompilerFactory;
import org.codehaus.commons.compiler.IExpressionEvaluator;
import org.codehaus.commons.compiler.IScriptEvaluator;
import org.codehaus.commons.compiler.ISimpleCompiler;
import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.compiler.WarningHandler;
import org.codehaus.commons.nullanalysis.Nullable;
import org.junit.Assert;

/**
 * A base class for JUnit 4 test cases that provides easy-to-use functionality to test JANINO.
 */
public
class CommonsCompilerTestSuite {

    /**
     * The version of the running JVM (6, 7, 8, ...). The version of the JRE on the bootstrap classpath may be
     * different from (i.e. smaller than) the JVM version!
     */
    public static final int JVM_VERSION;

    static {
        String  jv = System.getProperty("java.specification.version");
        Matcher m  = Pattern.compile("(?:1\\.)?(\\d+)").matcher(jv);
        Assert.assertTrue(m.matches());
        JVM_VERSION = Integer.parseInt(m.group(1));
    }

    public final boolean isJdk, isJanino;

    /**
     * The {@link ICompilerFactory} in effect for this test execution.
     */
    protected final ICompilerFactory compilerFactory;

    public
    CommonsCompilerTestSuite(ICompilerFactory compilerFactory) {

        this.compilerFactory = compilerFactory;

        String compilerFactoryId = compilerFactory.getId();
        this.isJdk    = compilerFactoryId.equals("org.codehaus.commons.compiler.jdk"); // SUPPRESS CHECKSTYLE EqualsAvoidNull
        this.isJanino = compilerFactoryId.equals("org.codehaus.janino");
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * Asserts that cooking the given <var>expression</var>, when cooked, issues an error.
     */
    protected void
    assertExpressionUncookable(String expression) throws Exception {
        new ExpressionTest(expression).assertUncookable();
    }

    /**
     * Asserts that cooking the given <var>expression</var>, when cooked, issues an error, and the error message
     * contains a match of the <var>messageRegex</var>.
     */
    protected void
    assertExpressionUncookable(String expression, String messageRegex) throws Exception {
        new ExpressionTest(expression).assertUncookable(messageRegex);
    }

    /**
     * Asserts that cooking the given <var>expression</var> issues an error, and the error is located at the given
     * <var>messageLineNumber</var>.
     */
    protected void
    assertExpressionUncookable(String expression, int messageLineNumber) throws Exception {
        new ExpressionTest(expression).assertUncookable(messageLineNumber);
    }

    /**
     * Asserts that the given <var>expression</var> can be cooked without errors and warnings.
     */
    protected void
    assertExpressionCookable(String expression) throws Exception {
        new ExpressionTest(expression).assertCookable();
    }

    /**
     * Asserts that the given <var>expression</var> can be cooked and evaluated. (Its value is ignored.)
     */
    protected void
    assertExpressionEvaluatable(String expression) throws Exception {
        new ExpressionTest(expression).assertExecutable();
    }

    /**
     * Asserts that the given <var>expression</var> is cookable and evaluates to TRUE.
     */
    protected void
    assertExpressionEvaluatesTrue(String expression) throws Exception {
        new ExpressionTest(expression).assertResultTrue();
    }

    /**
     * @return The result of the expression evaluation
     */
    protected Object
    evaluateExpression(String expression) throws Exception {
        ExpressionTest et = new ExpressionTest(expression);
        et.cook();
        return et.execute();
    }

    public
    class ExpressionTest extends CompileAndExecuteTest {

        private final String                 expression;
        protected final IExpressionEvaluator expressionEvaluator;

        public
        ExpressionTest(String expression) throws Exception {
            this.expression          = expression;
            this.expressionEvaluator = CommonsCompilerTestSuite.this.compilerFactory.newExpressionEvaluator();
        }

        @Override public void setSourceVersion(int sourceVersion)               { this.expressionEvaluator.setSourceVersion(sourceVersion);      }
        @Override public void setTargetVersion(int targetVersion)               { this.expressionEvaluator.setTargetVersion(targetVersion);      }
        @Override public void setCompileErrorHandler(ErrorHandler errorHandler) { this.expressionEvaluator.setCompileErrorHandler(errorHandler); }
        @Override public void setWarningHandler(WarningHandler warningHandler)  { this.expressionEvaluator.setWarningHandler(warningHandler);    }

        public void setParameters(String[] parameterNames, Class<?>[] parameterTypes) { this.expressionEvaluator.setParameters(parameterNames, parameterTypes); }

        @Override protected void
        cook() throws Exception {
            this.expressionEvaluator.cook(this.expression);

            // Enable assertions on the new class loader.
            this.expressionEvaluator.getMethod().getDeclaringClass().getClassLoader().setDefaultAssertionStatus(true);
        }

        @Override @Nullable protected Object
        execute() throws Exception {
            try {
                return this.expressionEvaluator.evaluate();
            } catch (InvocationTargetException ite) {
                Throwable te = ite.getTargetException();
                throw te instanceof Exception ? (Exception) te : ite;
            }
        }
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * Asserts that cooking the given <var>script</var> issues an error.
     */
    protected void
    assertScriptUncookable(String script) throws Exception {
        new ScriptTest(script).assertUncookable();
    }

    /**
     * Asserts that cooking the given <var>script</var> issues an error, and the error message contains a match for
     * <var>messageRegex</var>.
     */
    protected void
    assertScriptUncookable(String script, String messageRegex) throws Exception {
        new ScriptTest(script).assertUncookable(messageRegex);
    }

    /**
     * Asserts that cooking the given <var>script</var> issues an error, and the error is located at the given
     * <var>messageLineNumber</var>.
     */
    protected void
    assertScriptUncookable(String script, int messageLineNumber) throws Exception {
        new ScriptTest(script).assertUncookable(messageLineNumber);
    }

    /**
     * Asserts that the given <var>script</var> can be cooked without errors and warnings.
     */
    protected void
    assertScriptCookable(String script) throws Exception {
        new ScriptTest(script).assertCookable();
    }

    /**
     * Asserts that the given <var>script</var> can be cooked and executed. The return type of the scipt is {@code
     * void}.
     * Returns silently if cooking fails with a message that contains{@code "NYI"}.
     */
    protected void
    assertScriptExecutable(String script) throws Exception {
        new ScriptTest(script).assertExecutable();
    }

    /**
     * Asserts that the given <var>script</var> can be cooked and executed.
     *
     * @return The value returned by the script, or {@code null} if cooking failed with a message that contains
     *         {@code "NYI"}
     */
    protected <T> T
    assertScriptExecutable(String script, Class<T> returnType) throws Exception {

        final ScriptTest st = new ScriptTest(script);
        st.scriptEvaluator.setReturnType(returnType);

        @SuppressWarnings("unchecked") final T result = (T) st.assertExecutable();
        return result;
    }

    /**
     * Asserts that the given <var>script</var> is cookable and returns TRUE.
     */
    protected void
    assertScriptReturnsTrue(String script) throws Exception { new ScriptTest(script).assertResultTrue(); }

    protected void
    assertScriptReturnsNull(String script) throws Exception { new ScriptTest(script).assertResultNull(); }

    public
    class ScriptTest extends CompileAndExecuteTest {

        private final String             script;
        protected final IScriptEvaluator scriptEvaluator;

        public
        ScriptTest(String script) {
            this.script          = script;
            this.scriptEvaluator = CommonsCompilerTestSuite.this.compilerFactory.newScriptEvaluator();
            this.scriptEvaluator.setThrownExceptions(new Class<?>[] { Exception.class });
        }

        @Override public void setSourceVersion(int sourceVersion)               { this.scriptEvaluator.setSourceVersion(sourceVersion);      }
        @Override public void setTargetVersion(int targetVersion)               { this.scriptEvaluator.setTargetVersion(targetVersion);      }
        @Override public void setCompileErrorHandler(ErrorHandler errorHandler) { this.scriptEvaluator.setCompileErrorHandler(errorHandler); }
        @Override public void setWarningHandler(WarningHandler warningHandler)  { this.scriptEvaluator.setWarningHandler(warningHandler);    }

        @Override public void
        assertResultTrue() throws Exception {
            this.scriptEvaluator.setReturnType(boolean.class);
            super.assertResultTrue();
        }

        @Override public void
        assertResultNull() throws Exception {
            this.scriptEvaluator.setReturnType(Object.class);
            super.assertResultNull();
        }

        @Override protected void
        cook() throws Exception {
            this.scriptEvaluator.cook(this.script);

            // Enable assertions on the new class loader.
            this.scriptEvaluator.getMethod().getDeclaringClass().getClassLoader().setDefaultAssertionStatus(true);
        }

        @Override @Nullable protected Object
        execute() throws Exception {
            try {
                return this.scriptEvaluator.evaluate(new Object[0]);
            } catch (InvocationTargetException ite) {
                Throwable te = ite.getTargetException();
                throw te instanceof Exception ? (Exception) te : ite;
            }
        }
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * Asserts that cooking the given <var>classBody</var> issues an error.
     */
    protected void
    assertClassBodyUncookable(String classBody) throws Exception {
        new ClassBodyTest(classBody).assertUncookable();
    }

    /**
     * Asserts that cooking the given <var>classBody</var> issues an error, and the error message contains a match for
     * <var>messageRegex</var>.
     */
    protected void
    assertClassBodyUncookable(String classBody, String messageRegex) throws Exception {
        new ClassBodyTest(classBody).assertUncookable(messageRegex);
    }

    /**
     * Asserts that cooking the given <var>classBody</var> issues an error, and the error is located at the given
     * <var>messageLineNumber</var>.
     */
    protected void
    assertClassBodyUncookable(String classBody, int... messageLineNumber) throws Exception {
        new ClassBodyTest(classBody).assertUncookable(messageLineNumber);
    }

    /**
     * Asserts that the given <var>classBody</var> can be cooked (scanned, parsed, compiled and loaded) without errors
     * and warnings.
     */
    protected void
    assertClassBodyCookable(String classBody) throws Exception {
        new ClassBodyTest(classBody).assertCookable();
    }

    /**
     * Asserts that the given <var>classBody</var> is cookable and declares a method "{@code public [ static ]
     * }<em>any-type</em> {@code main()}" which executes and terminates normally. (The return value is ignored.)
     */
    protected void
    assertClassBodyExecutable(String classBody) throws Exception {
        new ClassBodyTest(classBody).assertExecutable();
    }

    /**
     * Asserts that the given <var>classBody</var> is cookable and declares a method "{@code public [ static ] boolean
     * main()}" which executes and returns {@code true}.
     */
    protected void
    assertClassBodyMainReturnsTrue(String classBody) throws Exception {
        new ClassBodyTest(classBody).assertResultTrue();
    }

    public
    class ClassBodyTest extends CompileAndExecuteTest {

        private final String                classBody;
        protected final IClassBodyEvaluator classBodyEvaluator;

        public
        ClassBodyTest(String classBody) throws Exception {
            this.classBody          = classBody;
            this.classBodyEvaluator = CommonsCompilerTestSuite.this.compilerFactory.newClassBodyEvaluator();
        }

        @Override public void setSourceVersion(int sourceVersion)               { this.classBodyEvaluator.setSourceVersion(sourceVersion);      }
        @Override public void setTargetVersion(int targetVersion)               { this.classBodyEvaluator.setTargetVersion(targetVersion);      }
        @Override public void setCompileErrorHandler(ErrorHandler errorHandler) { this.classBodyEvaluator.setCompileErrorHandler(errorHandler); }
        @Override public void setWarningHandler(WarningHandler warningHandler)  { this.classBodyEvaluator.setWarningHandler(warningHandler);    }

        @Override protected void
        cook() throws Exception {
            this.classBodyEvaluator.cook(this.classBody);

            // Enable assertions on the new class loader.
            this.classBodyEvaluator.getClazz().getClassLoader().setDefaultAssertionStatus(true);
        }

        @Override @Nullable protected Object
        execute() throws Exception {
            try {
                Class<?> cbeClass   = this.classBodyEvaluator.getClazz();
                Method   mainMethod = cbeClass.getMethod("main");
                return mainMethod.invoke(Modifier.isStatic(mainMethod.getModifiers()) ? null : cbeClass.newInstance());
            } catch (InvocationTargetException ite) {
                Throwable te = ite.getTargetException();
                throw te instanceof Exception ? (Exception) te : ite;
            }
        }
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * Asserts that cooking the given <var>compilationUnit</var> with the {@link ISimpleCompiler} issues an error.
     */
    protected void
    assertCompilationUnitUncookable(String compilationUnit) throws Exception {
        new SimpleCompilerTest(compilationUnit, "Xxx").assertUncookable();
    }

    /**
     * Asserts that cooking the given <var>compilationUnit</var> with the {@link ISimpleCompiler} issues an error, and
     * the error message contains a match for <var>messageRegex</var>.
     */
    protected void
    assertCompilationUnitUncookable(String compilationUnit, String messageRegex) throws Exception {
        new SimpleCompilerTest(compilationUnit, "Xxx").assertUncookable(messageRegex);
    }

    /**
     * Asserts that cooking the given <var>compilationUnit</var> with the {@link ISimpleCompiler} issues an error, and
     * the error is located at the given <var>messageLineNumber</var>.
     */
    protected void
    assertCompilationUnitUncookable(String compilationUnit, int messageLineNumber) throws Exception {
        new SimpleCompilerTest(compilationUnit, "Xxx").assertUncookable(messageLineNumber);
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler} without errors
     * and warnings.
     */
    protected void
    assertCompilationUnitCookable(String compilationUnit) throws Exception {
        new SimpleCompilerTest(compilationUnit, "Xxx").assertCookable();
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler} without errors
     * and warnings, <em>or</em> issues an error that matches the <var>messageRegex</var>.
     */
    protected void
    assertCompilationUnitCookable(String compilationUnit, String messageRegex) throws Exception {
        new SimpleCompilerTest(compilationUnit, "Xxx").assertCookable(messageRegex);
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler} and its '{@code
     * public static }<em>any-type className</em>{@code .main()}' method completes without exceptions. (The return
     * value is ignored.)
     */
    protected void
    assertCompilationUnitMainExecutable(String compilationUnit, String className) throws Exception {
        new SimpleCompilerTest(compilationUnit, className).assertExecutable();
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler} and its {@code
     * public static boolean }<em>className</em>{@code .main()} method returns TRUE.
     */
    protected void
    assertCompilationUnitMainReturnsTrue(String compilationUnit, String className) throws Exception {
        new SimpleCompilerTest(compilationUnit, className).assertResultTrue();
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler} and its {@code
     * public static boolean }<var>className</var>{@code .main()} method returns TRUE, <em>or</em> issues an error that matches
     * the <var>messageRegex</var>.
     */
    protected void
    assertCompilationUnitMainReturnsTrue(String compilationUnit, String className, String messageRegex) throws Exception {
        new SimpleCompilerTest(compilationUnit, className).assertResultTrue(messageRegex);
    }

    /**
     * Asserts that the given <var>compilationUnit</var> can be cooked by the {@link ISimpleCompiler}, and its {@code
     * public static any-type }<var>className</var>{@code .main()} method returns a value that equals <var>expected</var>.
     */
    protected void
    assertCompilationUnitMainEquals(Object expected, String compilationUnit, String className) throws Exception {
        new SimpleCompilerTest(compilationUnit, className).assertResultEquals(expected);
    }

    public
    class SimpleCompilerTest extends CompileAndExecuteTest {

        private final String            compilationUnit;
        private final String            className;
        protected final ISimpleCompiler simpleCompiler;

        public
        SimpleCompilerTest(String compilationUnit, String className) throws Exception {
            this.compilationUnit = compilationUnit;
            this.className       = className;
            this.simpleCompiler  = CommonsCompilerTestSuite.this.compilerFactory.newSimpleCompiler();
        }

        @Override public void setSourceVersion(int sourceVersion)               { this.simpleCompiler.setSourceVersion(sourceVersion);      }
        @Override public void setTargetVersion(int targetVersion)               { this.simpleCompiler.setTargetVersion(targetVersion);      }
        @Override public void setCompileErrorHandler(ErrorHandler errorHandler) { this.simpleCompiler.setCompileErrorHandler(errorHandler); }
        @Override public void setWarningHandler(WarningHandler warningHandler)  { this.simpleCompiler.setWarningHandler(warningHandler);    }

        @Override protected void
        cook() throws Exception {
            this.simpleCompiler.cook(this.compilationUnit);

            // Enable assertions on the new class loader.
            this.simpleCompiler.getClassLoader().setDefaultAssertionStatus(true);
        }

        @Override @Nullable protected Object
        execute() throws Exception {
            try {
                return (
                    this.simpleCompiler.getClassLoader()
                    .loadClass(this.className)
                    .getMethod("main", new Class[0])
                    .invoke(null, new Object[0])
                );
            } catch (InvocationTargetException ite) {
                Throwable te = ite.getTargetException();
                throw te instanceof Exception ? (Exception) te : ite;
            }
        }
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * Scans, parses, compiles and loads the class with the given <var>className</var> from the given
     * <var>sourceDirectory</var>.
     */
    protected void
    assertJavaSourceLoadable(final File sourceDirectory, final String className) throws Exception {
        AbstractJavaSourceClassLoader loader = this.compilerFactory.newJavaSourceClassLoader();
        loader.setSourcePath(new File[] { sourceDirectory });
        loader.loadClass(className);
    }

    // ----------------------------------------------------------------------------------------------------------------

    /**
     * A test case that calls its abstract methods {@link #cook()}, then {@link #execute()}, and verifies that they
     * throw exceptions or return results as expected.
     */
    private abstract
    class CompileAndExecuteTest {

        public abstract void setSourceVersion(int sourceVersion);
        public abstract void setTargetVersion(int targetVersion);
        public abstract void setCompileErrorHandler(ErrorHandler errorHandler);
        public abstract void setWarningHandler(WarningHandler warningHandler);

        /**
         * @see CompileAndExecuteTest
         */
        protected abstract void cook() throws Exception;

        /**
         * @return The value produced by the execution
         * @see    CompileAndExecuteTest
         */
        @Nullable protected abstract Object execute() throws Exception;

        /**
         * Asserts that cooking issues an error.
         *
         * @return The thrown {@link CompileException}
         */
        public CompileException
        assertUncookable() throws Exception {

            try {
                this.cook();
            } catch (CompileException ce) {
                return ce;
            }

            try {
                CommonsCompilerTestSuite.this.fail((
                    "Should have issued an error, but compiled successfully, and evaluated to \""
                    + this.execute()
                    + "\""
                ));
                throw new AssertionError();
            } catch (Exception e) {
                CommonsCompilerTestSuite.this.fail("Should have issued an error, but compiled successfully");
                throw new AssertionError();
            }
        }

        /**
         * Asserts that cooking issues an error, and that the error message contains the
         * <var>messageInfix</var>.
         */
        public void
        assertUncookable(@Nullable String messageRegex) throws Exception {

            CompileException ce           = this.assertUncookable();
            String           errorMessage = ce.getMessage();

            if (messageRegex != null && !Pattern.compile("(?s)" + messageRegex).matcher(errorMessage).find()) {
                CommonsCompilerTestSuite.this.fail((
                    "Error message '"
                    + errorMessage
                    + "' does not contain a match of '"
                    + messageRegex
                    + "'"
                ), ce);
            }
        }

        /**
         * Asserts that cooking issues an error, and that the error is located at the given
         * <var>expectedLineNumber</var>.
         */
        protected void
        assertUncookable(int... expectedLineNumber) throws Exception {

            CompileException ce = this.assertUncookable();

            Location loc = ce.getLocation();
            if (loc == null) throw new AssertionError("No location!?");

            if (CommonsCompilerTestSuite.indexOf(expectedLineNumber, loc.getLineNumber()) == -1) {
                CommonsCompilerTestSuite.this.fail((
                    "Compilation error (\""
                    + ce.getMessage()
                    + "\") expected in line "
                    + expectedLineNumber
                    + ", but was in line "
                    + loc.getLineNumber()
                ), ce);
            }
        }

        /**
         * Asserts that cooking completes without errors.
         */
        public void
        assertCookable() throws Exception {
            try {
                this.cook();
            } catch (CompileException ce) {
                throw new AssertionError(ce);
            }
        }

        /**
         * Asserts that cooking completes without errors, <em>or</em> issues an error that matches the
         * <var>messageRegex</var>.
         */
        protected void
        assertCookable(@Nullable String messageRegex) throws Exception {
            try {
                this.cook();
            } catch (CompileException ce) {
                String errorMessage = ce.getMessage();

                if (messageRegex != null && !Pattern.compile(messageRegex).matcher(errorMessage).find()) {
                    CommonsCompilerTestSuite.this.fail((
                        "Compilation error message \""
                        + errorMessage
                        + "\" does not contain a match of \""
                        + messageRegex
                        + "\""
                    ));
                }
            }
        }

        /**
         * Asserts that cooking and executing completes normally.
         *
         * @return The value produced by the execution, or {@code null} if cooking failed with a message that contains
         *         {@code "NYI"}
         */
        public Object
        assertExecutable() throws Exception {

            try {
                this.cook();
            } catch (CompileException ce) {

                // Have mercy with compile exceptions that have "NYI" ("not yet implemented") in their message.
                if (ce.getMessage().indexOf("NYI") != -1) return null;
                throw ce;
            }

            return this.execute();
        }

        /**
         * @return The result of the execution
         */
        @Nullable protected <T> T
        assertExecutable(Class<T> returnType) throws Exception {
            this.cook();

            @SuppressWarnings("unchecked") T result = (T) this.execute();
            return result;
        }

        /**
         * Asserts that cooking completes normally and executing returns {@link Boolean#TRUE}.
         */
        public void
        assertResultTrue() throws Exception {
            this.cook();
            Object result = this.execute();
            Assert.assertNotNull("Test result not NULL", result);
            Assert.assertSame(String.valueOf(result), Boolean.class, result.getClass());
            Assert.assertTrue("Test result is FALSE", (Boolean) result);
        }

        /**
         * Asserts that cooking completes normally and executing returns {@link Boolean#TRUE}, <em>or</em> issues an error that
         * matches the <var>messageRegex</var>.
         *
         * @param messageRegex {@code null} means "any error message is acceptable"
         */
        public void
        assertResultTrue(@Nullable String messageRegex) throws Exception {
            try {
                this.cook();
            } catch (CompileException ce) {
                String errorMessage = ce.getMessage();

                if (messageRegex != null && !Pattern.compile(messageRegex).matcher(errorMessage).find()) {
                    CommonsCompilerTestSuite.this.fail((
                        "Compilation error message \""
                        + errorMessage
                        + "\" does not contain a match of \""
                        + messageRegex
                        + "\""
                    ), ce);
                }
                return;
            }

            Object result = this.execute();
            Assert.assertNotNull("Test result not NULL", result);
            Assert.assertSame(String.valueOf(result), Boolean.class, result.getClass());
            Assert.assertTrue("Test result is FALSE", (Boolean) result);
        }

        /**
         * Asserts that cooking completes normally and executing returns a value that equals <var>expected</var>.
         */
        public void
        assertResultEquals(@Nullable Object expected) throws Exception {
            this.cook();
            Object result = this.execute();
            Assert.assertEquals(expected, result);
        }

        /**
         * Asserts that cooking completes normally and executing returns {@code null}.
         */
        public void
        assertResultNull() throws Exception {
            this.cook();
            Object result = this.execute();
            Assert.assertNull(String.valueOf(result), result);
        }
    }

    private void
    fail(@Nullable String message) { this.fail(message, null); }

    private void
    fail(@Nullable String message, @Nullable Throwable cause) {

        AssertionError ae = new AssertionError((
            message
            + " (implementation="
            + this.compilerFactory.getId()
            + ", java.version="
            + System.getProperty("java.version")
            + ")"
        ));

        if (cause != null) ae.initCause(cause);

        throw ae;
    }

    private static int
    indexOf(int[] ia, int subject) {
        for (int i = 0; i < ia.length; i++) {
            if (ia[i] == subject) return i;
        }
        return -1;
    }

    public static void
    assertLessThan(@Nullable String message, int expected, int actual) {
        Assert.assertTrue(
            (message == null ? "" : message + ": ") + "Expected less than " + expected + ", but were " + actual,
            actual < expected
        );
    }

    public static void
    assertMoreThan(@Nullable String message, int expected, int actual) {
        Assert.assertTrue(
            (message == null ? "" : message + ": ") + "Expected more than " + expected + ", but were " + actual,
            actual > expected
        );
    }
}