| /* |
| * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package vm.runtime.defmeth.shared.builder; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.util.*; |
| |
| import nsk.share.Pair; |
| |
| import static jdk.internal.org.objectweb.asm.Opcodes.*; |
| |
| import nsk.share.TestFailure; |
| import vm.runtime.defmeth.shared.ClassFileGenerator; |
| import vm.runtime.defmeth.shared.Constants; |
| import vm.runtime.defmeth.shared.DefMethTest; |
| import vm.runtime.defmeth.shared.DefMethTestFailure; |
| import vm.runtime.defmeth.shared.ExecutionMode; |
| import vm.runtime.defmeth.shared.executor.GeneratedTest; |
| import vm.runtime.defmeth.shared.MemoryClassLoader; |
| import vm.runtime.defmeth.shared.executor.MHInvokeWithArgsTest; |
| import vm.runtime.defmeth.shared.Printer; |
| import vm.runtime.defmeth.shared.executor.ReflectionTest; |
| import vm.runtime.defmeth.shared.executor.TestExecutor; |
| import vm.runtime.defmeth.shared.Util; |
| |
| import vm.runtime.defmeth.shared.data.Clazz; |
| import vm.runtime.defmeth.shared.data.ConcreteClass; |
| import vm.runtime.defmeth.shared.data.ConcreteClassLazyAdapter; |
| import vm.runtime.defmeth.shared.data.Interface; |
| import vm.runtime.defmeth.shared.data.InterfaceLazyAdapter; |
| import vm.runtime.defmeth.shared.data.Tester; |
| |
| /** |
| * Builder for test cases. |
| * |
| * Simplifies construction of test cases. |
| * |
| * Example: |
| * <code> |
| * TestBuilder b = new TestBuilder(); |
| * |
| * Interface I = b.intf("I").build(); |
| * ConcreteClazz C = b.clazz("C").implements(I).build(); |
| * |
| * b.test().callSite(I, C, "hashCode","()I").returns(0).build(); |
| * |
| * b.run(); |
| * </code> |
| * |
| * produces |
| * |
| * <code> |
| * interface I {} |
| * |
| * class C implements I {} |
| * |
| * class Test1 { |
| * public void test() { |
| * I i = new C(); |
| * if (c.hashCode() != 0) { |
| * throw new RuntimeException("Expected 0"); |
| * } |
| * } |
| * } |
| * </code> |
| */ |
| public class TestBuilder { |
| // Class file major version |
| // Used for separate compilation scenarios |
| public final int minMajorVer; |
| |
| // Additional access flags for all methods |
| public final int accFlags; |
| |
| // Redefine classes as part of testing |
| public final boolean redefineClasses; |
| |
| // Redefine classes using Retransformation API |
| public final boolean retransformClasses; |
| |
| public final ExecutionMode executionMode; |
| |
| // GeneratedTest counter which is used to name the tests. |
| private int testNo = 0; |
| |
| // List of classes constructed for the testcase so far |
| private List<Clazz> elements = new ArrayList<>(); |
| public List<Clazz> getElements() { |
| return new ArrayList<>(elements); |
| } |
| |
| // List of tests constructed as part of the testcase |
| private List<Tester> tests = new ArrayList<>(); |
| |
| // Elements under construction |
| private Set<Builder<?>> underConstruction = new LinkedHashSet<>(); |
| |
| private DefMethTest testInstance; |
| |
| TestBuilder(DefMethTest testInstance, int minMajorVer, int accFlags, |
| boolean redefineClasses, boolean retransformClasses, ExecutionMode executionMode) { |
| this.testInstance = testInstance; |
| this.minMajorVer = minMajorVer; |
| this.accFlags = accFlags; |
| this.redefineClasses = redefineClasses; |
| this.retransformClasses = retransformClasses; |
| this.executionMode = executionMode; |
| } |
| |
| /** |
| * Factory method for Interface builder. |
| * |
| * @param name |
| * @return class builder |
| */ |
| public InterfaceBuilder intf(String name) { |
| InterfaceBuilder b = new InterfaceBuilder(this).name(name); |
| underConstruction.add(b); |
| return b; |
| } |
| |
| /** |
| * Factory method for Clazz builder. |
| * |
| * @param name |
| * @return class builder |
| */ |
| public ConcreteClassBuilder clazz(String name) { |
| ConcreteClassBuilder b = new ConcreteClassBuilder(this).name(name).ver(minMajorVer); |
| underConstruction.add(b); |
| return b; |
| } |
| |
| /** |
| * Factory method for Tester builder. |
| * |
| * @return test builder |
| */ |
| public TesterBuilder test() { |
| TesterBuilder b = new TesterBuilder(++testNo, this); |
| underConstruction.add(b); |
| return b; |
| } |
| |
| /** |
| * Find previously constructed class by it's name. |
| * |
| * The method is considered safe: if it fails to find a class, it throws |
| * IllegalArgumentException. |
| * |
| * @param name |
| * @return |
| */ |
| public Clazz lookup(String name) { |
| for (Clazz clz : elements) { |
| if (clz.name().equals(name)) { |
| return clz; |
| } |
| } |
| |
| throw new IllegalArgumentException("Unknown element: " + name); |
| } |
| |
| /** |
| * Lazy binding of {@code Clazz} instance |
| * |
| * @param name |
| * @return |
| */ |
| public ConcreteClass clazzByName(String name) { |
| return new ConcreteClassLazyAdapter(this, name); |
| } |
| |
| /** |
| * Lazy binding of {@code Interface} instance |
| * |
| * @param name |
| * @return |
| */ |
| public Interface intfByName(String name) { |
| return new InterfaceLazyAdapter(this, name); |
| } |
| |
| /** |
| * Construct corresponding {@code Clazz} instance for a {@code Class}. |
| * |
| * @param cls |
| * @return |
| */ |
| public Clazz toClazz(Class cls) { |
| String name = cls.getName(); |
| |
| if (hasElement(name)) { |
| return lookup(name); |
| } else { |
| return clazz(name).build(); |
| } |
| } |
| |
| /** |
| * Construct corresponding {@code ConcreteClass} instance for a {@code Class}. |
| * |
| * Throws {@code IllegalArgumentException} if {@code Class} can't be |
| * represented as {@code ConcreteClass} |
| * |
| * @param cls |
| * @return |
| */ |
| public ConcreteClass toConcreteClass(Class cls) { |
| if (!cls.isInterface()) { |
| return (ConcreteClass)toClazz(cls); |
| } else { |
| throw new IllegalArgumentException(cls.getName()); |
| } |
| } |
| |
| /** |
| * Factory method for Method builder. |
| * |
| * @return method builder |
| */ |
| /* package-private */ MethodBuilder method() { |
| return new MethodBuilder(this); |
| } |
| |
| /** |
| * Factory method for Data.DefaultMethod builder. |
| * |
| * @param name method name |
| * @return |
| */ |
| public MethodBuilder defaultMethod(String name) { |
| return method().name(name).type(MethodType.DEFAULT); |
| } |
| |
| /** |
| * Factory method for Data.AbstractMethod builder. |
| * |
| * @param name |
| * @return |
| */ |
| public MethodBuilder abstractMethod(String name) { |
| return method().name(name).type(MethodType.ABSTRACT); |
| } |
| |
| /** |
| * Factory method for Data.ConcreteMethod builder. |
| * |
| * @param name |
| * @return |
| */ |
| public MethodBuilder concreteMethod(String name) { |
| return method().name(name).type(MethodType.CONCRETE); |
| } |
| |
| /** |
| * Signal that {@code Builder<?>} instance won't be used anymore. |
| * |
| * @param builder |
| */ |
| /* package-private */ void finishConstruction(Builder<?> builder) { |
| if (underConstruction.contains(builder)) { |
| underConstruction.remove(builder); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * Register class with the test builder, so it'll be enumerated during |
| * hierarchy traversals (e.g. class file generation, hierarchy printing). |
| * |
| * @param clz |
| * @return |
| */ |
| public TestBuilder register(Clazz clz) { |
| elements.add(clz); |
| return this; |
| } |
| |
| /** |
| * Register class with the test builder as a test, so it'll be enumerated during |
| * hierarchy traversals and executed during test runs. |
| * |
| * @param test |
| * @return |
| */ |
| public TestBuilder register(Tester test) { |
| tests.add(test); |
| return this; |
| } |
| |
| /** |
| * Check whether a class with some name has been already constructed. |
| * |
| * @param name |
| * @return whether a class with the same name has been already created |
| */ |
| /* package-private */ boolean hasElement(String name) { |
| for (Clazz clz : elements) { |
| if (clz.name().equals(name)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Create all classes and return a class loader which can load them. |
| * |
| * @return class loader instance which loads all generated classes |
| */ |
| public MemoryClassLoader build() { |
| Map<String,byte[]> classes = produce(); |
| |
| MemoryClassLoader cl = new MemoryClassLoader(classes); |
| |
| return cl; |
| } |
| |
| /** |
| * Produce class files for a set of {@code Clazz} instances. |
| * |
| * @return |
| */ |
| public Map<String,byte[]> produce() { |
| if (!underConstruction.isEmpty()) { |
| throw new IllegalStateException("Some of the classes haven't been fully constructed"); |
| } |
| |
| List<Clazz> items = new ArrayList<>(); |
| items.addAll(elements); |
| items.addAll(tests); |
| |
| return produce(52, items); |
| } |
| |
| /** |
| * Produce class files for {@Clazz} instances from {@code elements}. |
| * |
| * @param defaultMajorVer |
| * @param elements |
| * @return |
| */ |
| private Map<String,byte[]> produce(int defaultMajorVer, List<? extends Clazz> elements) { |
| LinkedHashMap<String,byte[]> classes = new LinkedHashMap<>(); |
| |
| if (Constants.PRINT_TESTS) { |
| System.out.printf("\nTEST: %s\n\n", Util.getTestName()); |
| } |
| |
| for (Clazz clazz : elements) { |
| if (Constants.PRINT_TESTS) { |
| System.out.println(Printer.print(clazz)); |
| } |
| |
| if (clazz instanceof Tester && |
| (executionMode == ExecutionMode.REFLECTION || |
| executionMode == ExecutionMode.INVOKE_WITH_ARGS)) { |
| // No need to generate testers for reflection cases |
| continue; |
| } |
| |
| Pair<String,byte[]> p = produceClassFile(defaultMajorVer, executionMode, clazz); |
| classes.put(p.first, p.second); |
| } |
| |
| if (Constants.PRINT_ASSEMBLY) { |
| System.out.println("\nDISASSEMBLY"); |
| |
| for (byte[] cf : classes.values()) { |
| Util.printClassFile(cf); |
| System.out.println(); |
| } |
| } |
| |
| if (Constants.ASMIFY) { |
| System.out.println("\nASM"); |
| |
| for (byte[] cf : classes.values()) { |
| Util.asmifyClassFile(cf); |
| System.out.println(); |
| } |
| } |
| |
| if (Constants.DUMP_CLASSES) { |
| try { |
| File dumpDir = new File("DUMP_CLASS_FILES", testInstance.shortTestName); |
| if (!dumpDir.exists()) { |
| dumpDir.mkdirs(); |
| } |
| |
| for (Map.Entry<String,byte[]> entry : classes.entrySet()) { |
| String name = entry.getKey(); |
| byte[] classFile = entry.getValue(); |
| File dumpFile = new File(dumpDir, name+".class"); |
| dumpFile.getParentFile().mkdirs(); |
| try (FileOutputStream file = new FileOutputStream(dumpFile)) { |
| file.write(classFile); |
| } |
| } |
| } catch (Exception e) { |
| throw new Error(e); |
| } |
| } |
| |
| return classes; |
| } |
| |
| /** |
| * Produce class file from {@code Clazz} instance. |
| * |
| * @param defaultMajorVer |
| * @param clazz |
| * @return |
| */ |
| public static Pair<String,byte[]> produceClassFile(int defaultMajorVer, |
| ExecutionMode execMode, Clazz clazz) { |
| int majorVer = clazz.ver() != 0 ? clazz.ver() : defaultMajorVer; |
| |
| ClassFileGenerator cfg = new ClassFileGenerator(majorVer, ACC_PUBLIC, execMode); |
| clazz.visit(cfg); |
| |
| byte[] classFile = cfg.getClassFile(); |
| |
| return Pair.of(clazz.name(), classFile); |
| } |
| |
| /** |
| * Make all preparations for execution. |
| * |
| * @return |
| */ |
| public TestExecutor prepare() { |
| return prepare(build()); |
| } |
| |
| private TestExecutor prepare(MemoryClassLoader cl) { |
| if (redefineClasses) { |
| try { |
| cl.modifyClasses(/* retransform = */ false); |
| } catch (TestFailure e) { |
| testInstance.getLog().info(e.getMessage()); |
| throw e; |
| } |
| } |
| |
| if (retransformClasses) { |
| try { |
| cl.modifyClasses(/* redefine = */ true); |
| } catch (TestFailure e) { |
| testInstance.getLog().info(e.getMessage()); |
| throw e; |
| } |
| } |
| |
| switch (executionMode) { |
| case DIRECT: |
| case INDY: |
| case INVOKE_EXACT: |
| case INVOKE_GENERIC: |
| // Run tests using direct invocation methods |
| return new GeneratedTest(cl, testInstance, tests); |
| case REFLECTION: |
| // Use reflection for testing |
| return new ReflectionTest(cl, this, testInstance, tests); |
| case INVOKE_WITH_ARGS: |
| return new MHInvokeWithArgsTest(cl, testInstance, tests); |
| default: |
| throw new Error("Unknown execution mode: " + executionMode); |
| } |
| } |
| |
| public interface LoaderConstructor { |
| public MemoryClassLoader construct(Map< String,byte[]> classFiles); |
| } |
| |
| /** |
| * Customize class loader construction. |
| * |
| * @param constructLoader |
| * @return |
| */ |
| public TestExecutor prepare(LoaderConstructor constructLoader) { |
| return prepare(constructLoader.construct(produce())); |
| } |
| |
| /** |
| * Construct a test with all necessary classes and execute all tests |
| * from it. |
| */ |
| public void run() { |
| if (tests.isEmpty()) { |
| throw new IllegalStateException("No tests to run"); |
| } |
| |
| TestExecutor executor = prepare(); |
| |
| List<Pair<Tester,Throwable>> errors = executor.run(); |
| |
| if (!errors.isEmpty()) { |
| throw new DefMethTestFailure(errors); |
| } |
| |
| } |
| } |