jdk/test/jdk/jdk/classfile/CorpusTest.java
2023-08-03 08:12:20 +00:00

291 lines
14 KiB
Java

/*
* Copyright (c) 2022, 2023, 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.
*/
/*
* @test
* @summary Testing Classfile on small Corpus.
* @build helpers.* testdata.*
* @run junit/othervm/timeout=480 -Djunit.jupiter.execution.parallel.enabled=true CorpusTest
*/
import helpers.ClassRecord;
import helpers.ClassRecord.CompatibilityFilter;
import helpers.Transforms;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.io.ByteArrayInputStream;
import java.util.*;
import static helpers.ClassRecord.assertEqualsDeep;
import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.api.Assertions.*;
import static helpers.TestUtil.assertEmpty;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import jdk.internal.classfile.Attributes;
import jdk.internal.classfile.BufWriter;
import jdk.internal.classfile.Classfile;
import jdk.internal.classfile.ClassTransform;
import jdk.internal.classfile.CodeTransform;
import jdk.internal.classfile.constantpool.ConstantPool;
import jdk.internal.classfile.constantpool.PoolEntry;
import jdk.internal.classfile.constantpool.Utf8Entry;
import jdk.internal.classfile.impl.DirectCodeBuilder;
import jdk.internal.classfile.impl.UnboundAttribute;
import jdk.internal.classfile.instruction.LineNumber;
import jdk.internal.classfile.instruction.LocalVariable;
import jdk.internal.classfile.instruction.LocalVariableType;
/**
* CorpusTest
*/
@Execution(ExecutionMode.CONCURRENT)
class CorpusTest {
protected static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/"));
protected static final String testFilter = null; //"modules/java.base/java/util/function/Supplier.class";
static void splitTableAttributes(String sourceClassFile, String targetClassFile) throws IOException, URISyntaxException {
var root = Paths.get(URI.create(CorpusTest.class.getResource("CorpusTest.class").toString())).getParent();
var cc = Classfile.of();
Files.write(root.resolve(targetClassFile), cc.transform(cc.parse(root.resolve(sourceClassFile)), ClassTransform.transformingMethodBodies((cob, coe) -> {
var dcob = (DirectCodeBuilder)cob;
var curPc = dcob.curPc();
switch (coe) {
case LineNumber ln -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.LINE_NUMBER_TABLE) {
@Override
public void writeBody(BufWriter b) {
b.writeU2(1);
b.writeU2(curPc);
b.writeU2(ln.line());
}
});
case LocalVariable lv -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.LOCAL_VARIABLE_TABLE) {
@Override
public void writeBody(BufWriter b) {
b.writeU2(1);
lv.writeTo(b);
}
});
case LocalVariableType lvt -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.LOCAL_VARIABLE_TYPE_TABLE) {
@Override
public void writeBody(BufWriter b) {
b.writeU2(1);
lvt.writeTo(b);
}
});
default -> cob.with(coe);
}
})));
// ClassRecord.assertEqualsDeep(
// ClassRecord.ofClassModel(ClassModel.of(Files.readAllBytes(root.resolve(targetClassFile)))),
// ClassRecord.ofClassModel(ClassModel.of(Files.readAllBytes(root.resolve(sourceClassFile)))));
// ClassPrinter.toYaml(ClassModel.of(Files.readAllBytes(root.resolve(targetClassFile))), ClassPrinter.Verbosity.TRACE_ALL, System.out::print);
}
static Path[] corpus() throws IOException, URISyntaxException {
splitTableAttributes("testdata/Pattern2.class", "testdata/Pattern2-split.class");
return Stream.of(
Files.walk(JRT.getPath("modules/java.base/java")),
Files.walk(JRT.getPath("modules"), 2).filter(p -> p.endsWith("module-info.class")),
Files.walk(Paths.get(URI.create(CorpusTest.class.getResource("CorpusTest.class").toString())).getParent()))
.flatMap(p -> p)
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class") && !p.endsWith("DeadCodePattern.class"))
.filter(p -> testFilter == null || p.toString().equals(testFilter))
.toArray(Path[]::new);
}
@ParameterizedTest
@MethodSource("corpus")
void testNullAdaptations(Path path) throws Exception {
byte[] bytes = Files.readAllBytes(path);
Optional<ClassRecord> oldRecord;
Optional<ClassRecord> newRecord;
Map<Transforms.NoOpTransform, Exception> errors = new HashMap<>();
Map<Integer, Integer> baseDups = findDups(bytes);
for (Transforms.NoOpTransform m : Transforms.NoOpTransform.values()) {
if (m == Transforms.NoOpTransform.ARRAYCOPY
|| m == Transforms.NoOpTransform.SHARED_3_NO_STACKMAP
|| m.name().startsWith("ASM"))
continue;
try {
byte[] transformed = m.shared && m.classTransform != null
? Classfile.of(Classfile.StackMapsOption.DROP_STACK_MAPS)
.transform(Classfile.of().parse(bytes), m.classTransform)
: m.transform.apply(bytes);
Map<Integer, Integer> newDups = findDups(transformed);
oldRecord = m.classRecord(bytes);
newRecord = m.classRecord(transformed);
if (oldRecord.isPresent() && newRecord.isPresent())
assertEqualsDeep(newRecord.get(), oldRecord.get(),
"Class[%s] with %s".formatted(path, m.name()));
switch (m) {
case SHARED_1, SHARED_2, SHARED_3, SHARED_3L, SHARED_3P:
if (newDups.size() > baseDups.size()) {
System.out.println(String.format("Incremental dups in file %s (%s): %s / %s", path, m, baseDups, newDups));
}
compareCp(bytes, transformed);
break;
case UNSHARED_1, UNSHARED_2, UNSHARED_3:
if (!newDups.isEmpty()) {
System.out.println(String.format("Dups in file %s (%s): %s", path, m, newDups));
}
break;
}
}
catch (Exception ex) {
System.err.printf("Error processing %s with %s: %s.%s%n", path, m.name(),
ex.getClass(), ex.getMessage());
ex.printStackTrace(System.err);
errors.put(m, ex);
}
}
if (!errors.isEmpty()) {
String msg = String.format("Failures for %s:%n", path)
+ errors.entrySet().stream()
.map(e -> {
Exception exception = e.getValue();
StackTraceElement[] trace = exception.getStackTrace();
return String.format(" Mode %s: %s (%s:%d)",
e.getKey(), exception.toString(),
trace.length > 0 ? trace[0].getClassName() : "unknown",
trace.length > 0 ? trace[0].getLineNumber() : 0);
})
.collect(joining("\n"));
fail(String.format("Errors in testNullAdapt: %s", msg));
}
}
@ParameterizedTest
@MethodSource("corpus")
void testReadAndTransform(Path path) throws IOException {
byte[] bytes = Files.readAllBytes(path);
var cc = Classfile.of();
var classModel = cc.parse(bytes);
assertEqualsDeep(ClassRecord.ofClassModel(classModel), ClassRecord.ofStreamingElements(classModel),
"ClassModel (actual) vs StreamingElements (expected)");
byte[] newBytes = cc.build(
classModel.thisClass().asSymbol(),
classModel::forEachElement);
var newModel = cc.parse(newBytes);
assertEqualsDeep(ClassRecord.ofClassModel(newModel, CompatibilityFilter.By_ClassBuilder),
ClassRecord.ofClassModel(classModel, CompatibilityFilter.By_ClassBuilder),
"ClassModel[%s] transformed by ClassBuilder (actual) vs ClassModel before transformation (expected)".formatted(path));
assertEmpty(newModel.verify(null));
//testing maxStack and maxLocals are calculated identically by StackMapGenerator and StackCounter
byte[] noStackMaps = Classfile.of(Classfile.StackMapsOption.DROP_STACK_MAPS)
.transform(newModel,
ClassTransform.transformingMethodBodies(CodeTransform.ACCEPT_ALL));
var noStackModel = cc.parse(noStackMaps);
var itStack = newModel.methods().iterator();
var itNoStack = noStackModel.methods().iterator();
while (itStack.hasNext()) {
assertTrue(itNoStack.hasNext());
var m1 = itStack.next();
var m2 = itNoStack.next();
var text1 = m1.methodName().stringValue() + m1.methodType().stringValue() + ": "
+ m1.code().map(c -> c.maxLocals() + " / " + c.maxStack()).orElse("-");
var text2 = m2.methodName().stringValue() + m2.methodType().stringValue() + ": "
+ m2.code().map(c -> c.maxLocals() + " / " + c.maxStack()).orElse("-");
assertEquals(text1, text2);
}
assertFalse(itNoStack.hasNext());
}
// @Test(enabled = false)
// public void checkDups() {
// Checks input files for dups -- and there are. Not clear this test has value.
// Tests above
// Map<Integer, Integer> dups = findDups(bytes);
// if (!dups.isEmpty()) {
// String dupsString = dups.entrySet().stream()
// .map(e -> String.format("%d -> %d", e.getKey(), e.getValue()))
// .collect(joining(", "));
// System.out.println(String.format("Duplicate entries in input file %s: %s", path, dupsString));
// }
// }
private void compareCp(byte[] orig, byte[] transformed) {
var cc = Classfile.of();
var cp1 = cc.parse(orig).constantPool();
var cp2 = cc.parse(transformed).constantPool();
for (int i = 1; i < cp1.entryCount(); i += cp1.entryByIndex(i).width()) {
assertEquals(cpiToString(cp1.entryByIndex(i)), cpiToString(cp2.entryByIndex(i)));
}
if (cp1.entryCount() != cp2.entryCount()) {
StringBuilder failMsg = new StringBuilder("Extra entries in constant pool (" + (cp2.entryCount() - cp1.entryCount()) + "): ");
for (int i = cp1.entryCount(); i < cp2.entryCount(); i += cp2.entryByIndex(i).width())
failMsg.append("\n").append(cp2.entryByIndex(i));
fail(failMsg.toString());
}
}
private static String cpiToString(PoolEntry e) {
String s = e.toString();
if (e instanceof Utf8Entry ue)
s = "CONSTANT_Utf8_info[value: \"%s\"]".formatted(ue.stringValue());
return s;
}
private static Map<Integer, Integer> findDups(byte[] bytes) {
Map<Integer, Integer> dups = new HashMap<>();
var cf = Classfile.of().parse(bytes);
var pool = cf.constantPool();
Set<String> entryStrings = new HashSet<>();
for (int i = 1; i < pool.entryCount(); i += pool.entryByIndex(i).width()) {
String s = cpiToString(pool.entryByIndex(i));
if (entryStrings.contains(s)) {
for (int j=1; j<i; j += pool.entryByIndex(j).width()) {
var e2 = pool.entryByIndex(j);
if (s.equals(cpiToString(e2)))
dups.put(i, j);
}
}
entryStrings.add(s);
}
return dups;
}
}