--- /dev/null 2017-11-09 09:38:01.297999907 +0100 +++ new/test/jdk/jdk/jfr/event/io/TestInstrumentation.java 2018-04-09 18:12:20.886034520 +0200 @@ -0,0 +1,374 @@ +/* + * Copyright (c) 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 jdk.jfr.event.io; + +import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; +import java.io.File; +import java.security.ProtectionDomain; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.IllegalClassFormatException; + +import jdk.internal.org.objectweb.asm.ClassReader; +import jdk.internal.org.objectweb.asm.ClassVisitor; +import jdk.internal.org.objectweb.asm.MethodVisitor; +import jdk.internal.org.objectweb.asm.ClassWriter; +import jdk.internal.org.objectweb.asm.Opcodes; +import jdk.internal.org.objectweb.asm.Type; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +/* + * @test + * @summary Test that will instrument the same classes that JFR will also instrument. + * @key jfr + * + * @library /test/lib /test/jdk + * @modules java.base/jdk.internal.org.objectweb.asm + * java.instrument + * jdk.jartool/sun.tools.jar + * jdk.jfr + * + * @run main/othervm jdk.jfr.event.io.TestInstrumentation + */ + +// Test that will instrument the same classes that JFR will also instrument. +// +// The methods that will be instrumented, for example java.io.RandomAccessFile.write, +// will add the following code at the start of the method: +// InstrumentationCallback.callback("::"); +// +// The class InstrumentationCallback will log all keys added by the callback() function. +// +// With this instrumentation in place, we will run some existing jfr.io tests +// to verify that our instrumentation has not broken the JFR instrumentation. +// +// After the tests have been run, we verify that the callback() function have been +// called from all instrumented classes and methods. This will verify that JFR has not +// broken our instrumentation. +// +// To use instrumentation, the test must be run in a new java process with +// the -javaagent option. +// We must also create two jars: +// TestInstrumentation.jar: The javaagent for the instrumentation. +// InstrumentationCallback.jar: This is a separate jar with the instrumentation +// callback() function. It is in a separate jar because it must be added to +// the bootclasspath to be called from java.io classes. +// +// The test contains 3 parts: +// Setup part that will create jars and launch the new test instance. +// Agent part that contains the instrumentation code. +// The actual test part is in the TestMain class. +// +public class TestInstrumentation implements ClassFileTransformer { + + private static Instrumentation instrumentation = null; + private static TestInstrumentation testTransformer = null; + + // All methods that will be instrumented. + private static final String[] instrMethodKeys = { + "java/io/RandomAccessFile::seek::(J)V", + "java/io/RandomAccessFile::read::()I", + "java/io/RandomAccessFile::read::([B)I", + "java/io/RandomAccessFile::write::([B)V", + "java/io/RandomAccessFile::write::(I)V", + "java/io/RandomAccessFile::close::()V", + "java/io/FileInputStream::read::([BII)I", + "java/io/FileInputStream::read::([B)I", + "java/io/FileInputStream::read::()I", + "java/io/FileOutputStream::write::(I)V", + "java/io/FileOutputStream::write::([B)V", + "java/io/FileOutputStream::write::([BII)V", + "java/net/SocketInputStream::read::()I", + "java/net/SocketInputStream::read::([B)I", + "java/net/SocketInputStream::read::([BII)I", + "java/net/SocketInputStream::close::()V", + "java/net/SocketOutputStream::write::(I)V", + "java/net/SocketOutputStream::write::([B)V", + "java/net/SocketOutputStream::write::([BII)V", + "java/net/SocketOutputStream::close::()V", + "java/nio/channels/FileChannel::read::([Ljava/nio/ByteBuffer;)J", + "java/nio/channels/FileChannel::write::([Ljava/nio/ByteBuffer;)J", + "java/nio/channels/SocketChannel::open::()Ljava/nio/channels/SocketChannel;", + "java/nio/channels/SocketChannel::open::(Ljava/net/SocketAddress;)Ljava/nio/channels/SocketChannel;", + "java/nio/channels/SocketChannel::read::([Ljava/nio/ByteBuffer;)J", + "java/nio/channels/SocketChannel::write::([Ljava/nio/ByteBuffer;)J", + "sun/nio/ch/FileChannelImpl::read::(Ljava/nio/ByteBuffer;)I", + "sun/nio/ch/FileChannelImpl::write::(Ljava/nio/ByteBuffer;)I", + }; + + private static String getInstrMethodKey(String className, String methodName, String signature) { + // This key is used to identify a class and method. It is sent to callback(key) + return className + "::" + methodName + "::" + signature; + } + + private static String getClassFromMethodKey(String methodKey) { + return methodKey.split("::")[0]; + } + + // Set of all classes targeted for instrumentation. + private static Set instrClassesTarget = null; + + // Set of all classes where instrumentation has been completed. + private static Set instrClassesDone = null; + + static { + // Split class names from InstrMethodKeys. + instrClassesTarget = new HashSet(); + instrClassesDone = new HashSet(); + for (String s : instrMethodKeys) { + String className = getClassFromMethodKey(s); + instrClassesTarget.add(className); + } + } + + private static void log(String msg) { + System.out.println("TestTransformation: " + msg); + } + + + //////////////////////////////////////////////////////////////////// + // This is the actual test part. + // A batch of jfr io tests will be run twice with a + // retransfromClasses() in between. After each test batch we verify + // that all callbacks have been called. + //////////////////////////////////////////////////////////////////// + + public static class TestMain { + + private enum TransformStatus { Transformed, Retransformed, Removed } + + public static void main(String[] args) throws Throwable { + runAllTests(TransformStatus.Transformed); + + // Retransform all classes and then repeat tests + Set> classes = new HashSet>(); + for (String className : instrClassesTarget) { + Class clazz = Class.forName(className.replaceAll("/", ".")); + classes.add(clazz); + log("Will retransform " + clazz.getName()); + } + instrumentation.retransformClasses(classes.toArray(new Class[0])); + + // Clear all callback keys so we don't read keys from the previous test run. + InstrumentationCallback.clear(); + runAllTests(TransformStatus.Retransformed); + + // Remove my test transformer and run tests again. Should not get any callbacks. + instrumentation.removeTransformer(testTransformer); + instrumentation.retransformClasses(classes.toArray(new Class[0])); + InstrumentationCallback.clear(); + runAllTests(TransformStatus.Removed); + } + + // This is not all available jfr io tests, but a reasonable selection. + public static void runAllTests(TransformStatus status) throws Throwable { + log("runAllTests, TransformStatus: " + status); + try { + String[] noArgs = new String[0]; + TestRandomAccessFileEvents.main(noArgs); + TestSocketEvents.main(noArgs); + TestSocketChannelEvents.main(noArgs); + TestFileChannelEvents.main(noArgs); + TestFileStreamEvents.main(noArgs); + TestDisabledEvents.main(noArgs); + + // Verify that all expected callbacks have been called. + Set callbackKeys = InstrumentationCallback.getKeysCopy(); + for (String key : instrMethodKeys) { + boolean gotCallback = callbackKeys.contains(key); + boolean expectsCallback = isClassInstrumented(status, key); + String msg = String.format("key:%s, expects:%b", key, expectsCallback); + if (gotCallback != expectsCallback) { + throw new Exception("Wrong callback() for " + msg); + } else { + log("Correct callback() for " + msg); + } + } + } catch (Throwable t) { + log("Test failed in phase " + status); + t.printStackTrace(); + throw t; + } + } + + private static boolean isClassInstrumented(TransformStatus status, String key) throws Throwable { + switch (status) { + case Retransformed: + return true; + case Removed: + return false; + case Transformed: + String className = getClassFromMethodKey(key); + return instrClassesDone.contains(className); + } + throw new Exception("Test error: Unknown TransformStatus: " + status); + } + } + + + //////////////////////////////////////////////////////////////////// + // This is the setup part. It will create needed jars and + // launch a new java instance that will run the internal class TestMain. + // This setup step is needed because we must use a javaagent jar to + // transform classes. + //////////////////////////////////////////////////////////////////// + + public static void main(String[] args) throws Throwable { + buildJar("TestInstrumentation", true); + buildJar("InstrumentationCallback", false); + launchTest(); + } + + private static void buildJar(String jarName, boolean withManifest) throws Throwable { + final String slash = File.separator; + final String packageName = "jdk/jfr/event/io".replace("/", slash); + System.out.println("buildJar packageName: " + packageName); + + String testClasses = System.getProperty("test.classes", "?"); + String testSrc = System.getProperty("test.src", "?"); + String jarPath = testClasses + slash + jarName + ".jar"; + String manifestPath = testSrc + slash + jarName + ".mf"; + String className = packageName + slash + jarName + ".class"; + + String[] args = null; + if (withManifest) { + args = new String[] {"-cfm", jarPath, manifestPath, "-C", testClasses, className}; + } else { + args = new String[] {"-cf", jarPath, "-C", testClasses, className}; + } + + log("Running jar " + Arrays.toString(args)); + sun.tools.jar.Main jarTool = new sun.tools.jar.Main(System.out, System.err, "jar"); + if (!jarTool.run(args)) { + throw new Exception("jar failed: args=" + Arrays.toString(args)); + } + } + + // Launch the test instance. Will run the internal class TestMain. + private static void launchTest() throws Throwable { + final String slash = File.separator; + + // Need to add jdk/lib/tools.jar to classpath. + String classpath = + System.getProperty("test.class.path", "") + File.pathSeparator + + System.getProperty("test.jdk", ".") + slash + "lib" + slash + "tools.jar"; + String testClassDir = System.getProperty("test.classes", "") + slash; + + String[] args = { + "-Xbootclasspath/a:" + testClassDir + "InstrumentationCallback.jar", + "--add-exports", "java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED", + "-classpath", classpath, + "-javaagent:" + testClassDir + "TestInstrumentation.jar", + "jdk.jfr.event.io.TestInstrumentation$TestMain" }; + OutputAnalyzer output = ProcessTools.executeTestJvm(args); + output.shouldHaveExitValue(0); + } + + + //////////////////////////////////////////////////////////////////// + // This is the java agent part. Used to transform classes. + // + // Each transformed method will add this call: + // InstrumentationCallback.callback("::"); + //////////////////////////////////////////////////////////////////// + + public static void premain(String args, Instrumentation inst) throws Exception { + instrumentation = inst; + testTransformer = new TestInstrumentation(); + inst.addTransformer(testTransformer, true); + } + + public byte[] transform( + ClassLoader classLoader, String className, Class classBeingRedefined, + ProtectionDomain pd, byte[] bytes) throws IllegalClassFormatException { + // Check if this class should be instrumented. + if (!instrClassesTarget.contains(className)) { + return null; + } + + boolean isRedefinition = classBeingRedefined != null; + log("instrument class(" + className + ") " + (isRedefinition ? "redef" : "load")); + + ClassReader reader = new ClassReader(bytes); + ClassWriter writer = new ClassWriter( + reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + CallbackClassVisitor classVisitor = new CallbackClassVisitor(writer); + reader.accept(classVisitor, 0); + instrClassesDone.add(className); + return writer.toByteArray(); + } + + private static class CallbackClassVisitor extends ClassVisitor { + private String className; + + public CallbackClassVisitor(ClassVisitor cv) { + super(Opcodes.ASM5, cv); + } + + @Override + public void visit( + int version, int access, String name, String signature, + String superName, String[] interfaces) { + cv.visit(version, access, name, signature, superName, interfaces); + className = name; + } + + @Override + public MethodVisitor visitMethod( + int access, String methodName, String desc, String signature, String[] exceptions) { + String methodKey = getInstrMethodKey(className, methodName, desc); + boolean isInstrumentedMethod = Arrays.asList(instrMethodKeys).contains(methodKey); + MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions); + if (isInstrumentedMethod && mv != null) { + mv = new CallbackMethodVisitor(mv, methodKey); + log("instrumented " + methodKey); + } + return mv; + } + } + + public static class CallbackMethodVisitor extends MethodVisitor { + private String logMessage; + + public CallbackMethodVisitor(MethodVisitor mv, String logMessage) { + super(Opcodes.ASM5, mv); + this.logMessage = logMessage; + } + + @Override + public void visitCode() { + mv.visitCode(); + String methodDescr = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String.class)); + String className = InstrumentationCallback.class.getName().replace('.', '/'); + mv.visitLdcInsn(logMessage); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "callback", methodDescr); + } + } + +}