| /* | |
| * Copyright (c) 2012, 2024, 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 | |
| * @bug 7064927 | |
| * @summary Verify LocalVariableTable (LVT) exists when passed to transform() on a retransform operation. | |
| * @author Daniel D. Daugherty | |
| * | |
| * @library /test/lib | |
| * @run build VerifyLocalVariableTableOnRetransformTest | |
| * @run compile -g DummyClassWithLVT.java | |
| * @run shell MakeJAR.sh retransformAgent | |
| * @run main/othervm -javaagent:retransformAgent.jar VerifyLocalVariableTableOnRetransformTest VerifyLocalVariableTableOnRetransformTest | |
| */ | |
| import java.io.*; | |
| import java.lang.instrument.Instrumentation; | |
| import java.lang.instrument.ClassFileTransformer; | |
| import java.net.*; | |
| import java.security.ProtectionDomain; | |
| import java.util.List; | |
| import java.util.regex.Pattern; | |
| import jdk.test.lib.JDKToolLauncher; | |
| import jdk.test.lib.process.ProcessTools; | |
| import jdk.test.lib.process.OutputAnalyzer; | |
| public class | |
| VerifyLocalVariableTableOnRetransformTest | |
| extends ATransformerManagementTestCase | |
| { | |
| private byte[] fTargetClassBytes; | |
| private boolean fTargetClassMatches; | |
| private String fTargetClassName = "DummyClassWithLVT"; | |
| private String classFileName = fTargetClassName + ".class"; | |
| private boolean fTargetClassSeen; | |
| private Throwable transformerException; | |
| /** | |
| * Constructor for VerifyLocalVariableTableOnRetransformTest. | |
| * @param name | |
| */ | |
| public VerifyLocalVariableTableOnRetransformTest(String name) | |
| { | |
| super(name); | |
| File f = originalClassFile(); | |
| System.out.println("Reading test class from " + f); | |
| try | |
| { | |
| InputStream redefineStream = new FileInputStream(f); | |
| fTargetClassBytes = NamedBuffer.loadBufferFromStream(redefineStream); | |
| System.out.println("Read " + fTargetClassBytes.length + " bytes."); | |
| } | |
| catch (IOException e) | |
| { | |
| fail("Could not load the class: " + f.getName()); | |
| } | |
| } | |
| public static void | |
| main (String[] args) | |
| throws Throwable { | |
| ATestCaseScaffold test = new VerifyLocalVariableTableOnRetransformTest(args[0]); | |
| test.runTest(); | |
| } | |
| protected final void | |
| doRunTest() | |
| throws Throwable { | |
| verifyClassFileBuffer(); | |
| } | |
| public void | |
| verifyClassFileBuffer() | |
| throws Throwable | |
| { | |
| beVerbose(); | |
| // With this call here, we will see the target class twice: | |
| // first when it gets loaded and second when it gets retransformed. | |
| addTransformerToManager(fInst, new MyObserver(), true); | |
| ClassLoader loader = getClass().getClassLoader(); | |
| Class target = loader.loadClass(fTargetClassName); | |
| assertEquals(fTargetClassName, target.getName()); | |
| // make an instance to prove the class was really loaded | |
| Object testInstance = target.newInstance(); | |
| // check for unexpected errors (see JDK-8311534) | |
| if (transformerException != null) { | |
| transformerException.printStackTrace(); | |
| throw new RuntimeException("Error in transformer" + transformerException); | |
| } | |
| assertTrue(fTargetClassName + " was not seen by transform()", | |
| fTargetClassSeen); | |
| // The HotSpot VM hands us class file bytes at initial class | |
| // load time that match the .class file contents. However, | |
| // according to the following spec that is not required: | |
| // http://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html#retransformClasses(java.lang.Class...) | |
| // This test exists to catch any unintentional change in | |
| // behavior by the HotSpot VM. If this behavior is intentionally | |
| // changed in the future, then this test will need to be | |
| // updated. | |
| compareClassFileBytes(true); | |
| fTargetClassSeen = false; | |
| fTargetClassMatches = false; | |
| fInst.retransformClasses(target); | |
| assertTrue(fTargetClassName + " was not seen by transform()", | |
| fTargetClassSeen); | |
| // The HotSpot VM doesn't currently preserve the LocalVariable | |
| // Table (LVT) attribute so the class file bytes seen by the | |
| // retransformClasses() call will not match the class file bytes | |
| // seen at initial class load time. | |
| compareClassFileBytes(false); | |
| } | |
| private File originalClassFile() { | |
| return new File(System.getProperty("test.classes", "."), classFileName); | |
| } | |
| private File transformedClassFile() { | |
| // This file will get created in the test execution | |
| // directory so there is no conflict with the file | |
| // in the test classes directory. | |
| return new File(classFileName); | |
| } | |
| private static final String[] expectedDifferentStrings = { | |
| "^Classfile .+$", | |
| "^[\\s]+SHA-256 checksum .[^\\s]+$" | |
| }; | |
| private boolean expectedDifferent(String line) { | |
| for (String s: expectedDifferentStrings) { | |
| Pattern p = Pattern.compile(s); | |
| if (p.matcher(line).find()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private void compareClassFileBytes(boolean initialLoad) throws Throwable { | |
| if (fTargetClassMatches) { | |
| return; | |
| } | |
| File f1 = originalClassFile(); | |
| File f2 = transformedClassFile(); | |
| System.out.println(fTargetClassName + " did not match .class file"); | |
| System.out.println("Disassembly difference (" + f1 + " vs " + f2 +"):"); | |
| // compare 'javap -v' output for the class files | |
| List<String> out1 = disassembleClassFile(f1); | |
| List<String> out2 = disassembleClassFile(f2); | |
| boolean different = false; | |
| boolean orderChanged = false; | |
| int lineNum = 0; | |
| for (String line: out1) { | |
| if (!expectedDifferent(line)) { | |
| if (!out2.contains(line)) { | |
| different = true; | |
| System.out.println("< (" + (lineNum + 1) + ") " + line); | |
| } else { | |
| if (lineNum < out2.size() && out1.get(lineNum) != out2.get(lineNum)) { | |
| // out2 contains line, but at different position | |
| orderChanged = true; | |
| } | |
| } | |
| } | |
| lineNum++; | |
| } | |
| lineNum = 0; | |
| for (String line: out2) { | |
| if (!expectedDifferent(line)) { | |
| if (!out1.contains(line)) { | |
| different = true; | |
| System.out.println("> (" + (lineNum + 1) + ") " + line); | |
| } | |
| } | |
| lineNum++; | |
| } | |
| // accordingly the spec orderChanged is fine, but we consider it as error | |
| // (see comments in verifyClassFileBuffer()) | |
| if (different || orderChanged) { | |
| fail(fTargetClassName + " (" + (initialLoad ? "load" : "retransform") + ") did not match .class file" | |
| + (different ? "" : " (only order changed)")); | |
| } | |
| } | |
| private List<String> disassembleClassFile(File file) throws Throwable { | |
| JDKToolLauncher javap = JDKToolLauncher.create("javap") | |
| .addToolArg("-v") | |
| .addToolArg(file.toString()); | |
| ProcessBuilder pb = new ProcessBuilder(javap.getCommand()); | |
| OutputAnalyzer out = ProcessTools.executeProcess(pb); | |
| return out.asLines(); | |
| } | |
| public class MyObserver implements ClassFileTransformer { | |
| public MyObserver() { | |
| } | |
| public String toString() { | |
| return MyObserver.this.getClass().getName(); | |
| } | |
| private void saveMismatchedBytes(byte[] classfileBuffer) { | |
| try { | |
| FileOutputStream fos = new FileOutputStream(transformedClassFile()); | |
| fos.write(classfileBuffer); | |
| fos.close(); | |
| } catch (IOException ex) { | |
| } | |
| } | |
| public byte[] | |
| transform( | |
| ClassLoader loader, | |
| String className, | |
| Class<?> classBeingRedefined, | |
| ProtectionDomain protectionDomain, | |
| byte[] classfileBuffer) { | |
| // String concatenation can cause LinkageError or ClassCircularityError (see JDK-8311534). | |
| // Need to log it for analysis. | |
| try { | |
| System.out.println(this + ".transform() sees '" + className | |
| + "' of " + classfileBuffer.length + " bytes."); | |
| } catch (Throwable t) { | |
| // Try to log. Save the error for handling by main thread if the logging fails. | |
| try { | |
| t.printStackTrace(); | |
| } catch (Throwable t1) { | |
| transformerException = t; | |
| } | |
| return null; | |
| } | |
| if (className.equals(fTargetClassName)) { | |
| fTargetClassSeen = true; | |
| if (classfileBuffer.length != fTargetClassBytes.length) { | |
| System.out.println("Warning: " + fTargetClassName | |
| + " lengths do not match."); | |
| fTargetClassMatches = false; | |
| saveMismatchedBytes(classfileBuffer); | |
| return null; | |
| } else { | |
| System.out.println("Info: " + fTargetClassName | |
| + " lengths match."); | |
| } | |
| for (int i = 0; i < classfileBuffer.length; i++) { | |
| if (classfileBuffer[i] != fTargetClassBytes[i]) { | |
| System.out.println("Warning: " + fTargetClassName | |
| + "[" + i + "]: '" + classfileBuffer[i] | |
| + "' != '" + fTargetClassBytes[i] + "'"); | |
| fTargetClassMatches = false; | |
| saveMismatchedBytes(classfileBuffer); | |
| return null; | |
| } | |
| } | |
| fTargetClassMatches = true; | |
| System.out.println("Info: verified '" + fTargetClassName | |
| + ".class' matches 'classfileBuffer'."); | |
| } | |
| return null; | |
| } | |
| } | |
| } |