blob: f1a413ddf7bd27f4b5e7235fa553cc0b9075a6e8 [file] [log] [blame]
/*
* 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);
}
}
}