--- /dev/null 2019-08-16 15:28:57.192077812 -0400 +++ new/test/jdk/java/lang/reflect/Proxy/ProxyGeneratorCombo.java 2019-08-21 16:28:43.592034130 -0400 @@ -0,0 +1,568 @@ +/* + * Copyright (c) 2011, 2019, 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. + */ + +/* + * @test + * @summary Proxy Generator Combo tests + * @library /test/langtools/tools/javac/lib . + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.code + * jdk.compiler/com.sun.tools.javac.comp + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.tree + * jdk.compiler/com.sun.tools.javac.util + * @build combo.ComboTestHelper + * @run main/othervm ProxyGeneratorCombo + * @run main/othervm -Djdk.proxy.ProxyGenerator.v49=true ProxyGeneratorCombo + */ + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import combo.ComboInstance; +import combo.ComboParameter; +import combo.ComboTask.Result; +import combo.ComboTestHelper; + +public class ProxyGeneratorCombo extends ComboInstance { + + // The unique number to qualify interface names, unique across multiple runs + private static int uniqueId = 0; + + /** + * Class Access kinds. + */ + enum ClassAccessKind implements ComboParameter { + PUBLIC("public"), + PACKAGE(""), + ; + + String classAccessTemplate; + + ClassAccessKind(String classAccessTemplate) { + this.classAccessTemplate = classAccessTemplate; + } + + @Override + public String expand(String optParameter) { + return classAccessTemplate; + } + } + + /** + * Signatures of methods to be tested. + */ + enum MethodsKind implements ComboParameter { + NONE(""), + ZERO("#{METHODACCESS} void zero() #{EXCEPTION};"), + ONE("#{METHODACCESS} void one(#{ARG[0]} a) #{EXCEPTION};"), + TWO("#{METHODACCESS} void one(#{ARG[0]} b);\n" + + "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);"), + THREE("#{METHODACCESS} void one(#{ARG[0]} a);\n" + + "#{METHODACCESS} void two(#{ARG[0]} a, #{ARG[1]} b);\n" + + "#{METHODACCESS} void three(#{ARG[0]} a, #{ARG[1]} b, #{ARG[0]} c);"); + + String methodsKindTemplate; + + MethodsKind(String methodsKindTemplate) { + this.methodsKindTemplate = methodsKindTemplate; + } + + @Override + public String expand(String optParameter) { + return methodsKindTemplate; + } + } + + /** + * Type of arguments to insert in method signatures + */ + enum ArgumentKind implements ComboParameter { + BOOLEAN("boolean"), + BYTE("byte"), + CHAR("char"), + SHORT("short"), + INT("int"), + LONG("long"), + FLOAT("float"), + DOUBLE("double"), + STRING("String"); + + String argumentsKindTemplate; + + ArgumentKind(String argumentsKindTemplate) { + this.argumentsKindTemplate = argumentsKindTemplate; + } + + @Override + public String expand(String optParameter) { + return argumentsKindTemplate; + } + } + + /** + * Exceptions to be added to zero and one methods. + */ + enum ExceptionKind implements ComboParameter { + NONE(null), + EXCEPTION(java.lang.Exception.class), + RUNTIME_EXCEPTION(java.lang.RuntimeException.class), + ILLEGAL_ARGUMENT_EXCEPTION(java.lang.IllegalArgumentException.class), + IOEXCEPTION(java.io.IOException.class), + /** + * Used only for throw testing, is empty for throws clause in the source, + */ + UNDECLARED_EXCEPTION(Exception1.class), + ; + + Class exceptionKindClass; + + ExceptionKind(Class exceptionKindClass) { + this.exceptionKindClass = exceptionKindClass; + } + + @Override + public String expand(String optParameter) { + return exceptionKindClass == null || exceptionKindClass == Exception1.class + ? "" : "throws " + exceptionKindClass.getName(); + } + } + + /** + * Extra interfaces to be added. + */ + enum MultiInterfacesKind implements ComboParameter { + NONE(new Class[0]), + INTERFACE_WITH_EXCEPTION(new Class[] {InterfaceWithException.class}), + ; + + Class[] multiInterfaceClasses; + + MultiInterfacesKind(Class[] multiInterfaceClasses) { + this.multiInterfaceClasses = multiInterfaceClasses; + } + + @Override + // Not used for expansion only execution + public String expand(String optParameter) { + throw new RuntimeException("NYI"); + } + + Class[] classes() { + return multiInterfaceClasses; + } + } + + @Override + public int id() { + return ++uniqueId; + } + + protected void fail(String msg, Throwable thrown) { + super.fail(msg); + thrown.printStackTrace(); + } + + /** + * Test interface with a "one(int)" method. + */ + interface InterfaceWithException { + // The signature must match the ONE MethodsKind above + void one(int a) throws RuntimeException, IOException; + } + + + /** + * Main to generate combinations and run the tests. + * @param args unused + * @throws Exception In case of failure + */ + public static void main(String... args) throws Exception { + + // Test variations of access declarations + new ComboTestHelper() + .withDimension("CLASSACCESS", ClassAccessKind.values()) + .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) + .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, + new MethodsKind[] {MethodsKind.NONE, MethodsKind.ZERO, MethodsKind.ONE}) + .withDimension("ARG[0]", new ArgumentKind[] {ArgumentKind.INT}) + .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, + new ExceptionKind[]{ExceptionKind.NONE}) + .run(ProxyGeneratorCombo::new); + + // Test variations of argument types + new ComboTestHelper() + .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) + .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) + .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, + MethodsKind.values()) + .withArrayDimension("ARG", ProxyGeneratorCombo::saveArg, 2, + ArgumentKind.values()) + .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, + new ExceptionKind[]{ExceptionKind.NONE}) + .withFilter(ProxyGeneratorCombo::filter) + .run(ProxyGeneratorCombo::new); + + // Test for conflicts in Exceptions on methods with the same signatures + new ComboTestHelper() + .withDimension("CLASSACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) + .withDimension("METHODACCESS", new ClassAccessKind[]{ClassAccessKind.PUBLIC}) + .withDimension("METHODS", ProxyGeneratorCombo::saveMethod, new MethodsKind[] { + MethodsKind.ZERO}) + .withDimension("EXCEPTION", ProxyGeneratorCombo::saveException, + ExceptionKind.values()) + .withDimension("MULTI_INTERFACES", ProxyGeneratorCombo::saveInterface, + new MultiInterfacesKind[] {MultiInterfacesKind.NONE}) + .run(ProxyGeneratorCombo::new); + } + + /** + * Basic template. + */ + String template = "#{CLASSACCESS} interface #{TESTNAME} {\n" + + "#{METHODS}" + + "}"; + + // Saved values of Combo values + private MultiInterfacesKind currInterface = MultiInterfacesKind.NONE; + private MethodsKind currMethod = MethodsKind.NONE; + private ExceptionKind currException = ExceptionKind.NONE; + private ArgumentKind[] currArgs = new ArgumentKind[0]; + + void saveInterface(ComboParameter s) { + currInterface = (MultiInterfacesKind)s; + } + + void saveMethod(ComboParameter s) { + currMethod = (MethodsKind)s; + } + + void saveException(ComboParameter s) { + currException = (ExceptionKind)s; + } + + void saveArg(ComboParameter s, int index) { + if (index >= currArgs.length) { + currArgs = Arrays.copyOf(currArgs, index + 1); + } + currArgs[index] = (ArgumentKind)s; + } + + /** + * Filter out needless tests (mostly with more variations of arguments than needed). + * @return true to run the test, false if not + */ + boolean filter() { + if ((currMethod == MethodsKind.NONE || currMethod == MethodsKind.ZERO) && + currArgs.length >= 2) { + return currArgs[0] == ArgumentKind.INT && + currArgs[1] == ArgumentKind.INT; + } + if (currMethod == MethodsKind.ONE && + currArgs.length >= 2 ) { + return currArgs[0] == currArgs[1]; + } + return true; + } + + /** + * Generate the source file and compile. + * Generate a proxy for the interface and test the resulting Proxy + * for the methods, exceptions and handling of a thrown exception + * @throws IOException catch all IOException + */ + @Override + public void doWork() throws IOException { + String cp = System.getProperty("test.classes"); + String ifaceName = "Interface_" + this.id(); + newCompilationTask() + .withSourceFromTemplate(ifaceName, template.replace("#{TESTNAME}", ifaceName)) + .withOption("-d") + .withOption(cp) + .generate(this::checkCompile); + try { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + Class tc = Class.forName(ifaceName); + InvocationHandler handler = + new ProxyHandler(currException.exceptionKindClass); + + // Construct array of interfaces for the proxy + Class[] interfaces = new Class[currInterface.classes().length + 1]; + interfaces[0] = tc; + System.arraycopy(currInterface.classes(), 0, + interfaces, 1, + currInterface.classes().length); + + Object proxy = Proxy.newProxyInstance(loader, interfaces, handler); + if (!Proxy.isProxyClass(proxy.getClass())) { + fail("generated proxy is not a proxy class"); + return; + } + for (Class i : interfaces) { + if (!i.isAssignableFrom(proxy.getClass())) { + fail("proxy is not assignable to " + i.getName()); + } + } + try { + String s = proxy.toString(); + } catch (Exception ex) { + ex.printStackTrace(); + fail("proxy.toString() threw an exception"); + } + + checkDeclaredProxyExceptions(proxy, interfaces); + + if (currMethod == MethodsKind.ZERO && currException != ExceptionKind.NONE) { + checkThrowsException(proxy, interfaces); + } + + } catch (Exception ex) { + throw new RuntimeException("doWork unexpected", ex); + } + } + + /** + * Check that the exceptions declared on the proxy match the declarations for + * exceptions from the interfaces. + * + * @param proxy a proxy object + * @param interfaces the interfaces that defined it + */ + void checkDeclaredProxyExceptions(Object proxy, Class[] interfaces) { + var allMethods = allMethods(Arrays.asList(interfaces)); + Method[] methods = proxy.getClass().getDeclaredMethods(); + for (Method m : methods) { + String sig = toShortSignature(m); + var imethods = allMethods.get(sig); + if (imethods != null) { + var expectedEx = Set.copyOf(Arrays.asList(m.getExceptionTypes())); + var exs = Set.copyOf(extractExceptions(imethods)); + if (!expectedEx.equals(exs)) { + System.out.printf("mismatch on exceptions for method %s:%nExpected: " + + "%s%nActual: %s%n", + sig, expectedEx, exs); + fail("Exceptions declared on proxy don't match interface methods"); + } + } + } + } + + void checkThrowsException(Object proxy, Class[] interfaces) { + ProxyHandler ph = (ProxyHandler)(Proxy.getInvocationHandler(proxy)); + try { + Method m = proxy.getClass().getDeclaredMethod("zero"); + m.invoke(proxy); + fail("Missing exception: " + ph.exceptionClass); + } catch (NoSuchMethodException nsme) { + System.out.printf("No method 'zero()' to test exceptions with%n"); + for (var cl : interfaces) { + System.out.printf(" i/f %s: %s%n", cl, Arrays.toString(cl.getMethods())); + } + Method[] methods = proxy.getClass().getMethods(); + System.out.printf(" Proxy methods: %s%n", Arrays.toString(methods)); + fail("No such method test bug", nsme); + } catch (InvocationTargetException actual) { + ph.checkThrownException(actual.getTargetException()); + } catch (IllegalAccessException iae) { + fail("IllegalAccessException", iae); + } + } + + /** + * Exceptions known to be supported by all methods with the same signature. + * @return a list of universal exception types + */ + private static List> extractExceptions(List methods) { + // for all methods with the same signature + // start with the exceptions from the first method + // while there are any exceptions remaining + // look at the next method + List> exceptions = null; + for (Method m : methods) { + var e = m.getExceptionTypes(); + if (e.length == 0) + return emptyClassList(); + List> elist = Arrays.asList(e); + if (exceptions == null) { + exceptions = elist; // initialize to first method exceptions + } else { + // for each exception + // if it is compatible (both ways) with any of the existing exceptions continue + // else remove the current exception + var okExceptions = new HashSet>(); + for (int j = 0; j < exceptions.size(); j++) { + var ex = exceptions.get(j); + for (int i = 0; i < elist.size();i++) { + var ci = elist.get(i); + + if (ci.isAssignableFrom(ex)) { + okExceptions.add(ex); + } + if (ex.isAssignableFrom(ci)) { + okExceptions.add(ci); + } + } + } + if (exceptions.isEmpty()) { + // The empty set terminates the search for a common set of exceptions + return emptyClassList(); + } + // Use the new set for the next iteration + exceptions = List.copyOf(okExceptions); + } + } + return (exceptions == null) ? emptyClassList() : exceptions; + } + + /** + * An empty correctly typed list of classes. + * @return An empty typed list of classes + */ + @SuppressWarnings("unchecked") + static List> emptyClassList() { + return Collections.EMPTY_LIST; + } + + /** + * Accumulate all of the unique methods. + * + * @param interfaces a list of interfaces + * @return a map from signature to List of methods, unique by signature + */ + private static Map> allMethods(List> interfaces) { + Map> methods = new HashMap<>(); + for (Class c : interfaces) { + for (Method m : c.getMethods()) { + if (!Modifier.isStatic(m.getModifiers())) { + String sig = toShortSignature(m); + methods.computeIfAbsent(sig, s -> new ArrayList()) + .add(m); + } + } + } + return methods; + } + + /** + * The signature of a method without the return type. + * @param m a Method + * @return the signature with method name and parameters + */ + static String toShortSignature(Method m) { + StringJoiner sj = new StringJoiner(",", m.getName() + "(", ")"); + for (Class parameterType : m.getParameterTypes()) { + sj.add(parameterType.getTypeName()); + } + return sj.toString(); + } + + /** + * Report any compilation errors. + * @param res the result + */ + void checkCompile(Result res) { + if (res.hasErrors()) { + fail("invalid diagnostics for source:\n" + + res.compilationInfo() + + "\nFound error: " + res.hasErrors()); + } + } + + /** + * The Handler for the proxy includes the method to invoke the proxy + * and the expected exception, if any. + */ + class ProxyHandler implements InvocationHandler { + + private final Class exceptionClass; + + ProxyHandler(Class throwable) { + this.exceptionClass = throwable; + } + + /** + * Invoke a method on the proxy or return a value. + * @param proxy the proxy instance that the method was invoked on + * @param method a method + * @param args some args + * @return + * @throws Throwable a throwable + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("toString")) { + return "Proxy" + System.identityHashCode(proxy); + } + if (method.getName().equals("zero")) { + if (exceptionClass != null) { + throw exceptionClass.getDeclaredConstructor().newInstance(); + } + } + return "meth: " + method.toString(); + } + + /** + * Check that the expected exception was thrown. + * Special case is handled for Exception1 which does not appear in the + * throws clause of the method so UndeclaredThrowableException is expected. + */ + void checkThrownException(Throwable thrown) { + if (exceptionClass == Exception1.class && + thrown instanceof UndeclaredThrowableException && + ((UndeclaredThrowableException)thrown).getUndeclaredThrowable() instanceof Exception1) { + // Exception1 caused UndeclaredThrowableException + return; + } else if (exceptionClass == Exception1.class) { + fail("UndeclaredThrowableException", thrown); + } + + if (exceptionClass != null && + !exceptionClass.equals(thrown.getClass())) { + throw new RuntimeException("Wrong exception thrown: expected: " + exceptionClass + + ", actual: " + thrown.getClass()); + } + } + } + + /** + * Exception to be thrown as a test of InvocationTarget. + */ + static class Exception1 extends Exception { + private static final long serialVersionUID = 1L; + Exception1() {} + } +}