Skip to content

Commit

Permalink
Add testcase for RuntimeDistCleaner (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte authored Jun 11, 2024
1 parent 73b9200 commit f119ede
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.common.asm;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.function.Consumer;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.fml.loading.ModFileBuilder;
import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class RuntimeDistCleanerTest {
@TempDir
Path tempDir;

@Test
void testStripInterface() throws Exception {
transformTestClass(Dist.CLIENT, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT, _interface = java.lang.AutoCloseable.class)
public class Test implements AutoCloseable {
public void close() {}
}
""", clazz -> assertThat(clazz.getInterfaces()).contains(AutoCloseable.class));
}

@Test
void testKeepInterface() throws Exception {
transformTestClass(Dist.DEDICATED_SERVER, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT, _interface = java.lang.AutoCloseable.class)
public class Test implements AutoCloseable {
public void close() {}
}
""", clazz -> assertThat(clazz.getInterfaces()).doesNotContain(AutoCloseable.class));
}

@Test
void testRejectLoadingClientClassOnDedicatedServer() throws Exception {
assertThatThrownBy(() -> transformTestClass(Dist.DEDICATED_SERVER, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT)
public class Test {
}
""")).hasMessageContaining("Attempted to load class test/Test for invalid dist DEDICATED_SERVER");
}

@Test
void testRejectLoadingDedicatedServerClassOnClient() throws Exception {
assertThatThrownBy(() -> transformTestClass(Dist.CLIENT, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.DEDICATED_SERVER)
public class Test {
}
""")).hasMessageContaining("Attempted to load class test/Test for invalid dist CLIENT");
}

@Test
void testAllowLoadingClientClassOnClient() throws Exception {
transformTestClass(Dist.CLIENT, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT)
public class Test {
}
""");
}

@Test
void testAllowLoadingDedicatedServerClassOnDedicatedServer() throws Exception {
transformTestClass(Dist.DEDICATED_SERVER, """
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.DEDICATED_SERVER)
public class Test {
}
""");
}

@Test
void testRemoveField() throws Exception {
transformTestClass(Dist.DEDICATED_SERVER, """
public class Test {
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT)
public int field;
}
""", clazz -> assertThat(clazz.getFields()).extracting(Field::getName).doesNotContain("field"));
}

@Test
void testRemoveMethod() throws Exception {
transformTestClass(Dist.DEDICATED_SERVER, """
public class Test {
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT)
public void method() {}
}
""", clazz -> assertThat(clazz.getDeclaredMethods())
// Coverage on Gradle with jacoco inserts methods
.filteredOn(m -> !m.getName().contains("jacoco"))
.isEmpty());
}

@Test
void testRemoveLambdasInsideOfMethod() throws Exception {
transformTestClass(Dist.DEDICATED_SERVER, """
import java.util.function.IntSupplier;
public class Test {
@net.neoforged.api.distmarker.OnlyIn(value = net.neoforged.api.distmarker.Dist.CLIENT)
public IntSupplier method(int arg) {
return () -> arg + 1;
}
}
""", clazz -> assertThat(clazz.getDeclaredMethods())
// Coverage on Gradle with jacoco inserts methods
.filteredOn(m -> !m.getName().contains("jacoco"))
.isEmpty());
}

private void transformTestClass(Dist dist, @Language("java") String classContent) throws Exception {
transformTestClass(dist, classContent, ignored -> {});
}

private void transformTestClass(Dist dist, @Language("java") String classContent, Consumer<Class<?>> asserter) throws Exception {
var distCleaner = new RuntimeDistCleaner();
distCleaner.getExtension().accept(dist);

var modJar = tempDir.resolve("modjar.jar");
new ModFileBuilder(modJar)
.addClass("test.Test", classContent)
.withTransform((type, classNode) -> {
var phases = distCleaner.handlesClass(type, false);
for (var phase : phases) {
distCleaner.processClassWithFlags(phase, classNode, type, "");
}
return classNode;
})
.build();

try (var cl = new URLClassLoader(new URL[] {
modJar.toUri().toURL()
})) {
var testClass = cl.loadClass("test.Test");
asserter.accept(testClass);
}
}
}
75 changes: 71 additions & 4 deletions loader/src/test/java/net/neoforged/fml/loading/ModFileBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import net.neoforged.fml.test.RuntimeCompiler;
Expand All @@ -30,8 +32,12 @@
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
import org.intellij.lang.annotations.Language;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;

public class ModFileBuilder implements Closeable {
public class ModFileBuilder {
public static final ContainedVersion JIJ_V1 = new ContainedVersion(VersionRange.createFromVersion("1.0"), new DefaultArtifactVersion("1.0"));

private final RuntimeCompiler compiler;
Expand All @@ -42,6 +48,7 @@ public class ModFileBuilder implements Closeable {
private final List<IdentifiableContent> content = new ArrayList<>();
private final Manifest manifest = new Manifest();
private final List<ContainedJarMetadata> jijEntries = new ArrayList<>();
private final List<BiFunction<Type, ClassNode, ClassNode>> transforms = new ArrayList<>();

// Info that will end up in the mods.toml

Expand Down Expand Up @@ -145,7 +152,15 @@ public ModFileBuilder withJarInJar(ContainedJarIdentifier identifier, ContainedV
return this;
}

public void build() throws IOException {
/**
* Statically applies the given class-node transforms to each class in the jar-file.
*/
public ModFileBuilder withTransform(BiFunction<Type, ClassNode, ClassNode> transform) {
this.transforms.add(transform);
return this;
}

public Path build() throws IOException {
compilationBuilder.compile();

if (!jijEntries.isEmpty()) {
Expand Down Expand Up @@ -185,9 +200,61 @@ public void build() throws IOException {
}
}
}

if (!transforms.isEmpty()) {
transformJar(destination, transforms);
}

close();

return destination;
}

private static void transformJar(Path destination,
List<BiFunction<Type, ClassNode, ClassNode>> transforms) throws IOException {
var transformedJar = destination.resolveSibling(destination.getFileName() + ".transformed");

// In the absence of a transforming class-loader, pre-transform everything
try (var in = new JarInputStream(Files.newInputStream(destination))) {
try (var out = new JarOutputStream(Files.newOutputStream(transformedJar), in.getManifest())) {
for (var entry = in.getNextEntry(); entry != null; entry = in.getNextEntry()) {
var path = entry.getName();

out.putNextEntry(new JarEntry(path));
if (path.endsWith(".class")) {
var classData = in.readAllBytes();
var classNode = new ClassNode();
ClassReader classReader = new ClassReader(classData);
classReader.accept(classNode, ClassReader.EXPAND_FRAMES);

// TRANSFORM CLASS
var classType = getTypeFromPath(path);
for (var transform : transforms) {
classNode = transform.apply(classType, classNode);
}

var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
classNode.accept(cw);
out.write(cw.toByteArray());
} else {
in.transferTo(out);
}
out.closeEntry();
}
}
}

// Move it over
Files.move(transformedJar, destination, StandardCopyOption.REPLACE_EXISTING);
}

private static Type getTypeFromPath(String entry) {
// Remove .class suffix
var className = entry.substring(0, entry.length() - ".class".length());

return Type.getType("L" + className + ";");
}

@Override
public void close() throws IOException {
compiler.close();
}
Expand Down

0 comments on commit f119ede

Please sign in to comment.