CompilerTest.java

/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010 Arno Unkrig. All rights reserved.
 * Copyright (c) 2015-2016 TIBCO Software Inc. All rights reserved. // CHECKSTYLE:OFF CHECKSTYLE:ON
 *
 * 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 org.codehaus.commons.compiler.tests;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.ErrorHandler;
import org.codehaus.commons.compiler.ICompiler;
import org.codehaus.commons.compiler.ICompilerFactory;
import org.codehaus.commons.compiler.ISimpleCompiler;
import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.compiler.lang.ClassLoaders;
import org.codehaus.commons.compiler.util.Benchmark;
import org.codehaus.commons.compiler.util.Disassembler;
import org.codehaus.commons.compiler.util.ResourceFinderClassLoader;
import org.codehaus.commons.compiler.util.reflect.ByteArrayClassLoader;
import org.codehaus.commons.compiler.util.resource.DirectoryResourceFinder;
import org.codehaus.commons.compiler.util.resource.MapResourceCreator;
import org.codehaus.commons.compiler.util.resource.MapResourceFinder;
import org.codehaus.commons.compiler.util.resource.MultiResourceFinder;
import org.codehaus.commons.compiler.util.resource.Resource;
import org.codehaus.commons.compiler.util.resource.ResourceFinder;
import org.codehaus.commons.compiler.util.resource.StringResource;
import org.codehaus.commons.nullanalysis.Nullable;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import util.TestUtil;

// SUPPRESS CHECKSTYLE JavadocMethod:9999

/**
 * Unit tests for the {@link SimpleCompiler}.
 */
@RunWith(Parameterized.class) public
class CompilerTest {

    private static final String COMMONS_COMPILER_SRC     = "../commons-compiler/src/main/java";
    private static final String COMMONS_COMPILER_JDK_SRC = "../commons-compiler-jdk/src/main/java";
    private static final String JANINO_SRC               = "../janino/src/main/java";
    private static final String JANINO8_SRC              = "../janino8/src";
    private static final String RESOURCE_DIR             = "src/test/resources";

    /**
     * The {@link ICompilerFactory} in effect for this test execution.
     */
    private final ICompilerFactory                    compilerFactory;
    private final String                              compilerFactoryId;
    private final boolean                             isJdk;
    @SuppressWarnings("unused") private final boolean isJanino;

    @Parameters(name = "CompilerFactory={0}") public static Collection<Object[]>
    compilerFactories() throws Exception { return TestUtil.getCompilerFactoriesForParameters(); }

    public
    CompilerTest(ICompilerFactory compilerFactory) {

        this.compilerFactory = compilerFactory;

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

    @Before
    public void
    setUp() throws Exception {
    }

    /**
     * Another attempt to reproduce issue #32... still no success.
     */
    @Ignore
    @Test public void
    testSelfCompileParallel() throws Exception {

        final Throwable[] ex = new Throwable[1];

        Runnable r = new Runnable() {

            @Override public void
            run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        System.out.printf("#%d%n", i);
                        CompilerTest.this.testSelfCompile();
                    }
                } catch (Throwable t) { // SUPPRESS CHECKSTYLE IllegalCatch
                    ex[0] = t;
                }
            }
        };

        Thread[] ts = new Thread[10];
        for (int i = 0; i < ts.length; i++) {
            (ts[i] = new Thread(r)).start();
        }
        for (int i = 0; i < ts.length && ex[0] == null; i++) {
            ts[i].join();
        }

        if (ex[0] != null) throw new AssertionError(ex[0]);
    }

    @Test public void
    testSelfCompile() throws Exception {

        File[] sourceFiles  = {
            (
                this.isJdk
                ? new File(CompilerTest.COMMONS_COMPILER_JDK_SRC + "/org/codehaus/commons/compiler/jdk/Compiler.java")
                : this.compilerFactory.getId().equals("org.codehaus.janino8")
                ? new File(CompilerTest.JANINO8_SRC              +              "/org/codehaus/janino8/Compiler.java")
                : new File(CompilerTest.JANINO_SRC               +               "/org/codehaus/janino/Compiler.java")
            ),
            new File(CompilerTest.COMMONS_COMPILER_SRC     + "/org/codehaus/commons/compiler/samples/ExpressionDemo.java"),
            new File(CompilerTest.COMMONS_COMPILER_SRC     + "/org/codehaus/commons/compiler/util/resource/MapResourceCreator.java"),
            new File(CompilerTest.COMMONS_COMPILER_SRC     + "/org/codehaus/commons/compiler/util/resource/MapResourceFinder.java"),
        };

        Benchmark b = new Benchmark(true);

        // -------------------- PHASE 1

        SortedMap<String, byte[]> classFileMap1;
        {
            b.beginReporting("Compile " + this.compilerFactoryId + " from scratch");
            classFileMap1 = new TreeMap<>(
                CompilerTest.compileJanino(sourceFiles, this.compilerFactory.newCompiler(), null)
            );
            b.endReporting("Generated " + classFileMap1.size() + " class files.");

            CompilerTest.assertMoreThan("Number of generated classes", 70, classFileMap1.size());
        }

        // -------------------- PHASE 2

        {
            b.beginReporting(
                "Compile " + this.compilerFactoryId + " again, but with the class files created during the first "
                + "compilation being available, i.e. only the explicitly given source files should be recompiled"
            );
            SortedMap<String, byte[]> classFileMap2 = new TreeMap<>(
                CompilerTest.compileJanino(sourceFiles, this.compilerFactory.newCompiler(), classFileMap1)
            );
            b.endReporting("Generated " + classFileMap2.size() + " class files.");

            CompilerTest.assertLessThan("Number of generated classes", 20, classFileMap2.size());
        }

        // -------------------- PHASE 3

        {

            // Set up a ClassLoader for the classes generated in phase 1.
            final ClassLoader cl = new ResourceFinderClassLoader(
                new MultiResourceFinder(
                    new MapResourceFinder(classFileMap1),
                    new DirectoryResourceFinder(new File("../de.unkrig.jdisasm/bin"))
                ),
                ClassLoaders.BOOTCLASSPATH_CLASS_LOADER
            );

            // Load the ICompiler from that class loader.
            b.beginReporting(
                "Compile " + this.compilerFactoryId + " again, but this time using the classes generated by the first "
                + "compilation, a.k.a. \"compile Janino with itself\""
            );

            Object iCompiler = cl.loadClass(
                this.compilerFactory.getId() + ".Compiler"
            ).getDeclaredConstructor().newInstance();

            SortedMap<String, byte[]> classFileMap3 = new TreeMap<>(CompilerTest.compileJanino(
                sourceFiles,
                iCompiler,
                null // precompiledClasses
            ));
            b.endReporting("Generated " + classFileMap3.size() + " class files.");

            // Compare "classFileMap1" and "classFileMap3". We cannot use "Map.equals()" because we
            // want to check byte-by-byte identity rather than reference identity.
            Assert.assertEquals(classFileMap1.keySet(), classFileMap3.keySet());
            for (Map.Entry<String, byte[]> me : classFileMap1.entrySet()) {
                String resourceName = me.getKey();

                byte[] expectedClassFileBytes = me.getValue();
                byte[] actualClassFileBytes   = classFileMap3.get(resourceName);
                if (!Arrays.equals(expectedClassFileBytes, actualClassFileBytes)) {
                    System.out.println("Expected:");
                    Disassembler.disassembleToStdout(expectedClassFileBytes);
                    System.out.println("Actual:");
                    Disassembler.disassembleToStdout(actualClassFileBytes);
                }
                Assert.assertArrayEquals(resourceName, expectedClassFileBytes, actualClassFileBytes);
            }
        }
    }

    /**
     * Compiles the current source of JANINO with the <var>compiler</var>.
     * The <var>compiler</var> may originate from a different class loader; all accesses to it are made through
     * reflection.
     *
     * @return The generated class files
     */
    private static Map<String, byte[]>
    compileJanino(File[] sourceFiles, Object compiler, @Nullable Map<String, byte[]> precompiledClasses)
    throws Exception {

        final ClassLoader cl = compiler.getClass().getClassLoader();

        final Class<?> DirectoryResourceFinder_class = cl.loadClass("org.codehaus.commons.compiler.util.resource.DirectoryResourceFinder"); // SUPPRESS CHECKSTYLE LocalFinalVariableName:6
        final Class<?> MapResourceCreator_class      = cl.loadClass("org.codehaus.commons.compiler.util.resource.MapResourceCreator");
        final Class<?> MapResourceFinder_class       = cl.loadClass("org.codehaus.commons.compiler.util.resource.MapResourceFinder");
        final Class<?> MultiResourceFinder_class     = cl.loadClass("org.codehaus.commons.compiler.util.resource.MultiResourceFinder");
        final Class<?> ResourceCreator_class         = cl.loadClass("org.codehaus.commons.compiler.util.resource.ResourceCreator");
        final Class<?> ResourceFinder_class          = cl.loadClass("org.codehaus.commons.compiler.util.resource.ResourceFinder");

//        compiler.setSourceFinder(new MultiResourceFinder(new ResourceFinder[] {
//             new DirectoryResourceFinder(new File(CompilerTest.JANINO_SRC)),
//             new DirectoryResourceFinder(new File(CompilerTest.COMMONS_COMPILER_SRC)),
//             new DirectoryResourceFinder(new File(CompilerTest.COMMONS_COMPILER_JDK_SRC)),
//         }));
        {
            final Object[] rfs = CompilerTest.newArray(
                ResourceFinder_class,
                CompilerTest.newInstance1(DirectoryResourceFinder_class, File.class, new File(CompilerTest.JANINO_SRC)),
                CompilerTest.newInstance1(DirectoryResourceFinder_class, File.class, new File(CompilerTest.COMMONS_COMPILER_SRC)),
                CompilerTest.newInstance1(DirectoryResourceFinder_class, File.class, new File(CompilerTest.COMMONS_COMPILER_JDK_SRC))
            );
            CompilerTest.invoke1(
                compiler,                  // target
                "setSourceFinder",         // methodName
                ResourceFinder_class,      // parameterType
                CompilerTest.newInstance1( // argument
                    MultiResourceFinder_class, // clasS
                    rfs.getClass(),            // parameterType
                    rfs                        // arguments
                )
            );
        }

//        compiler.setClassPath(new File[0]);
        CompilerTest.invoke1(compiler, "setClassPath", new File[0].getClass(), new File[0]);

        final Map<String, byte[]> result = new HashMap<>();

//        compiler.setClassFileCreator(new MapResourceCreator(result));
        CompilerTest.invoke1(
            compiler,
            "setClassFileCreator",
            ResourceCreator_class,
            CompilerTest.newInstance1(MapResourceCreator_class, Map.class, result)
        );

        if (precompiledClasses != null) {

//            MapResourceFinder classFileFinder = new MapResourceFinder(precompiledClasses);
            Object classFileFinder = CompilerTest.newInstance1(MapResourceFinder_class, Map.class, precompiledClasses);

//            classFileFinder.setLastModified(System.currentTimeMillis());
            CompilerTest.invoke1(classFileFinder, "setLastModified", long.class, System.currentTimeMillis());

//            compiler.setClassFileFinder(classFileFinder);
            CompilerTest.invoke1(compiler, "setClassFileFinder", ResourceFinder_class, classFileFinder);
        } else {

//            compiler.setClassFileFinder(ResourceFinder.EMPTY_RESOURCE_FINDER);
            CompilerTest.invoke1(
                compiler,
                "setClassFileFinder",
                ResourceFinder_class,
                CompilerTest.getStaticField(ResourceFinder_class, "EMPTY_RESOURCE_FINDER")
            );
        }

        // compiler.setDebugLines(true);
        // compiler.setDebugSource(true);
        // compiler.setDebugVars(true);
//        invoke1(compiler, "setDebugLines",  boolean.class, true);
//        invoke1(compiler, "setDebugSource", boolean.class, true);
//        invoke1(compiler, "setDebugVars",   boolean.class, true);

//        compiler.compile(sourceFiles);
        CompilerTest.invoke1(compiler, "compile", File[].class, sourceFiles);

        return result;
    }

    private static Object
    getStaticField(Class<?> clasS, String fieldName) throws Exception {
        return clasS.getDeclaredField(fieldName).get(null);
    }

    /**
     * Creates and returns an object instance through a one-argument constructor.
     */
    private static Object
    newInstance1(
        Class<?> clasS,
        Class<?> parameterType,
        Object   argument
    ) throws Exception {
        return CompilerTest.newInstance(clasS, new Class[] { parameterType }, new Object[] { argument });
    }

    private static Object
    newInstance(
        Class<?>   clasS,
        Class<?>[] parameterTypes,
        Object[]   arguments
    ) throws Exception {
        assert arguments.length == parameterTypes.length;
        return clasS.getDeclaredConstructor(parameterTypes).newInstance(arguments);
    }

    private static void
    invoke1(
        Object   target,
        String   methodName,
        Class<?> parameterType,
        Object   argument
    ) throws Exception {
        CompilerTest.invoke(target, methodName, new Class[] { parameterType }, new Object[] { argument });
    }

    private static void
    invoke(
        Object     target,
        String     methodName,
        Class<?>[] parameterTypes,
        Object[]   arguments
    ) throws Exception {
        assert arguments.length == parameterTypes.length;
        target.getClass().getMethod(methodName, parameterTypes).invoke(target, arguments);
    }

    private static Object[]
    newArray(Class<?> elementClass, Object... initializers) {
        final Object[] result = (Object[]) Array.newInstance(elementClass, initializers.length);
        for (int i = 0; i < initializers.length; i++) {
            result[i] = initializers[i];
        }
        return result;
    }

    @Test public void
    testTypeBug() throws Exception {

        File[] sourceFiles = {
            new File(
                CompilerTest.COMMONS_COMPILER_SRC + "/org/codehaus/commons/compiler/java8/java/util/stream/Stream.java"
            ),
        };
        ResourceFinder sourceFinder = new MultiResourceFinder(
            new DirectoryResourceFinder(new File(CompilerTest.JANINO_SRC)),
            new DirectoryResourceFinder(new File(CompilerTest.COMMONS_COMPILER_SRC))
        );

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

        Benchmark b = new Benchmark(true);
        b.beginReporting("Compile Stream.java");
        MapResourceCreator classFileResources1 = new MapResourceCreator();
        {
            ICompiler c = this.compilerFactory.newCompiler();
            c.setSourceFinder(sourceFinder);
            c.setClassPath(new File[0]);
            c.setClassFileCreator(classFileResources1);
//            c.setDebugLines(true);
//            c.setDebugSource(true);
//            c.setDebugVars(true);
            c.setCompileErrorHandler(new ErrorHandler() {

                @Override public void
                handleError(String message, @Nullable Location location) throws CompileException {
                    throw new CompileException(message, location);
                }
            });
            c.compile(sourceFiles);
        }
        Map<String, byte[]> classFileMap1 = classFileResources1.getMap();
        b.endReporting("Generated " + classFileMap1.size() + " class files.");
    }

    @Test public void
    testCompileErrors() throws Exception {

        MapResourceFinder sourceFinder = new MapResourceFinder();

        sourceFinder.addResource("pkg/A.java", ( // Class A uses class B, C, D.
            ""
            + "package pkg;\n"
            + "public class A {\n"
            + "    void meth() {\n"
            + "        new B();\n"
            + "        new C();\n"
            + "        new D();\n"
            + "    }\n"
            + "}\n"
        ));
        sourceFinder.addResource("pkg/B.java", (
            ""
            + "package pkg;\n"
            + "public class B {\n"
            + "}\n"
        ));
        sourceFinder.addResource("pkg/C.java", ( // Class C contains a compile error.
            ""
            + "package pkg;\n"
            + "public class C extends E {\n" // Compile error, because a class "E" is not defined.
            + "}\n"
        ));
        sourceFinder.addResource("pkg/D.java", (
            ""
            + "package pkg;\n"
            + "public class D {\n"
            + "}\n"
        ));

        this.assertUncompilable("cannot.*\\bE\\b", sourceFinder);
    }

    @Test public void
    testErrorHandler() throws Exception {
        MapResourceFinder sourceFinder = new MapResourceFinder();

        sourceFinder.addResource("pkg/A.java", (
            ""
            + "package pkg;\n"
            + "public class A {\n"
            + "    void meth() {\n"
            + "        return 1;\n"
            + "    }\n"
            + "}\n"
        ));

        // Default error handling.
        this.assertUncompilable(
            (
                ""
                + "Method must not return a value"
                + "|"
                + "unexpected return value"
                + "|"
                + "compiler\\.err\\.cant\\.ret\\.val\\.from\\.meth\\.decl\\.void"
                + "|"
                + "cannot return a value from method whose result type is void"
            ),
            sourceFinder
        );

        // Error handler that throws a CompileException.
        {
            ICompiler compiler = this.compilerFactory.newCompiler();

            final int[] count = new int[1];
            compiler.setCompileErrorHandler(new ErrorHandler() {
                @Override public void handleError(String message, @Nullable Location location) throws CompileException {
                    count[0]++;
                    throw new CompileException(message, location);
                }
            });

            CompilerTest.assertUncompilable(
                (
                    ""
                    + "Method must not return a value"
                    + "|"
                    + "unexpected return value"
                    + "|"
                    + "cannot return a value from method whose result type is void"
                ),
                compiler,
                sourceFinder
            );
            Assert.assertEquals(1, count[0]);
        }

        // Error handler that does *not* throw a CompileException.
        {
            ICompiler compiler = this.compilerFactory.newCompiler();

            final int[] count = new int[1];
            compiler.setCompileErrorHandler(new ErrorHandler() {
                @Override public void handleError(String message, @Nullable Location location) { count[0]++; }
            });

            CompilerTest.assertUncompilable(
                "Compilation failed with 1 errors|error.*while compiling|unknown reason",
                compiler,
                sourceFinder
            );
            Assert.assertEquals(1, count[0]);
        }
    }

    @Test public void
    testInMemoryCompilation() throws Exception {

        // Set of compilation units.
        MapResourceFinder sourceFinder = new MapResourceFinder();
        sourceFinder.addResource("pkg1/A.java", (
            ""
            + "package pkg1;\n"
            + "\n"
            + "import pkg2.*;\n"
            + "\n"
            + "public\n"
            + "class A {\n"
            + "    public static String main() { return B.meth(); }\n"
            + "    public static String meth() { return \"HELLO\"; }\n"
            + "}\n"
        ));
        sourceFinder.addResource("pkg2/B.java", (
            ""
            + "package pkg2;\n"
            + "\n"
            + "import pkg1.*;\n"
            + "\n"
            + "public\n"
            + "class B {\n"
            + "    public static String meth() { return A.meth(); }\n"
            + "}\n"
        ));

        final Map<String, byte[]> classes = this.compile(sourceFinder);
        Assert.assertEquals(2, classes.size());

        // Set up a class loader that finds and defined the generated classes.
        ClassLoader cl = new ByteArrayClassLoader(classes);

        // Now invoke "pkg1.A.main()" and assert that it returns "HELLO".
        Assert.assertEquals("HELLO", cl.loadClass("pkg1.A").getMethod("main").invoke(null));
    }

    private Map<String, byte[]>
    compile(MapResourceFinder sourceFinder) throws CompileException, IOException {

        // Set up the compiler.
        ICompiler compiler = this.compilerFactory.newCompiler();
        compiler.setSourceFinder(sourceFinder);
        return CompilerTest.compile(compiler, sourceFinder);
    }

    private static Map<String, byte[]>
    compile(ICompiler compiler, MapResourceFinder sourceFinder) throws CompileException, IOException {
//        compiler.setIClassLoader(new ClassLoaderIClassLoader(CompilerTest.class.getClassLoader()));

        // Storage for generated bytecode.
        final Map<String, byte[]> classes = new HashMap<>();
        compiler.setClassFileFinder(new MapResourceFinder(classes));
        compiler.setClassFileCreator(new MapResourceCreator(classes));
//        compiler.setDebugLines(true);
//        compiler.setDebugSource(true);
//        compiler.setDebugVars(true);

        // Compile all sources.
        compiler.compile(sourceFinder.resources().toArray(new Resource[0]));

        return classes;
    }

    @Test public void
    testImplicitCastTernaryOperator() throws Exception {

        String cu = (
            ""
            + "package pkg;\n"
            + "public class A {\n"
            + "    public static final Boolean wrap(boolean b) { return Boolean.valueOf(b); }\n"
            + "    public java.lang.Object one() { \n"
            + "       return this.f == (byte) 2 ? null : wrap(this.f == (byte) 1);\n"
            + "    }\n"
            + ""
            + "    public void two() {\n"
            + "       byte b = (byte) ((((byte) 2 == (byte) 2 ? (byte) 2 : (byte) 1 ^ (byte) 2)));\n"
            + "    }\n"
            + ""
            + "    public byte f = (byte) 2;\n"
            + "}\n"
        );

        ISimpleCompiler sc = this.compilerFactory.newSimpleCompiler();
        sc.cook(cu);
    }

    // https://github.com/codehaus/janino/issues/5
    @Test public void
    testLocalVarTableGeneration() throws Exception {
        ISimpleCompiler sc = this.compilerFactory.newSimpleCompiler();
        sc.setDebuggingInformation(true, true, true);
        sc.cook(new FileInputStream(CompilerTest.RESOURCE_DIR + "/a/TestLocalVarTable.java"));
        sc.getClassLoader().loadClass("a.TestLocalVarTable");
    }

    @Test public void
    testIssue98() throws Exception {

        ICompiler compiler = this.compilerFactory.newCompiler();

        // Here's the magic: Configure a custom "resource creator", so the .class files are stored in a Map, and no
        // files are created.
        Map<String, byte[]> classes = new HashMap<>();
        compiler.setClassFileCreator(new MapResourceCreator(classes));

        // Now compile two units with different package declarations.
        compiler.compile(new Resource[] {
            new StringResource("pkg1/A.java", "package pkg1; public class A { public static int meth() { return pkg2.B.meth(); } }"),
            new StringResource("pkg2/B.java", "package pkg2; public class B { public static int meth() { return 77;            } }"),
        });

        // Set up a class loader that uses the generated .class files.
        ClassLoader cl = new ResourceFinderClassLoader(
            new MapResourceFinder(classes),    // resourceFinder
            ClassLoader.getSystemClassLoader() // parent
        );

        // Invoke "pkg1.A.meth()" and verify that the return value is correct.
        Assert.assertEquals(77, cl.loadClass("pkg1.A").getDeclaredMethod("meth").invoke(null));
    }

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

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

    private static void
    assertFind(final String regex, final String actual) {
        Assert.assertTrue(
            "Expected that \"" + actual + "\" contain match of regex \"" + regex + "\"",
            Pattern.compile(regex, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(actual).find()
        );
    }

    private void
    assertUncompilable(String messageRegex, MapResourceFinder sourceFinder) throws IOException {
        try {
            this.compile(sourceFinder);
            Assert.fail("CompileException expected");
        } catch (CompileException ex) {
            CompilerTest.assertFind(messageRegex, ex.getMessage());
        }
    }

    private static void
    assertUncompilable(String messageRegex, ICompiler compiler, MapResourceFinder sourceFinder) throws IOException {
        try {
            CompilerTest.compile(compiler, sourceFinder);
            Assert.fail("CompileException expected");
        } catch (CompileException ex) {
            CompilerTest.assertFind(messageRegex, ex.getMessage());
        }
    }
}