Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions critter/core/src/test/java/dev/morphia/critter/ClassfileOutput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package dev.morphia.critter;

import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;

import org.jboss.windup.decompiler.fernflower.FernflowerDecompiler;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.TraceClassVisitor;

public class ClassfileOutput {

public static void dump(CritterClassLoader classLoader, String className) throws Exception {
dump(classLoader, className, Path.of("target/dumps/"));
}

public static void dump(CritterClassLoader classLoader, String className, Path outputDir) throws Exception {
byte[] bytes = classLoader.getTypeDefinitions().get(className);
if (bytes == null) {
String resourceName = className.replace('.', '/') + ".class";
InputStream stream = classLoader.getResourceAsStream(resourceName);
if (stream == null)
return;
bytes = stream.readAllBytes();
}
String[][] outputs = {
{ "javap", dumpBytecode(bytes) },
{ "asm", dumpAsmSource(bytes) },
{ "java", decompile(bytes) }
};
for (String[] entry : outputs) {
Path output = outputDir.resolve(className.replace('.', '/') + "." + entry[0]);
output.toFile().getParentFile().mkdirs();
Files.writeString(output, entry[1]);
}
}

public static void dump(String className, byte[] bytes) throws Exception {
dump(className, bytes, Path.of("target/dumps/"));
}

public static void dump(String className, byte[] bytes, Path outputDir) throws Exception {
String[][] outputs = {
{ "javap", dumpBytecode(bytes) },
{ "asm", dumpAsmSource(bytes) },
{ "java", decompile(bytes) }
};
for (String[] entry : outputs) {
Path output = outputDir.resolve(className + "." + entry[0]);
output.toFile().getParentFile().mkdirs();
Files.writeString(output, entry[1]);
}
}

public static String dumpBytecode(Class<?> clazz) throws Exception {
ClassReader classReader = new ClassReader(clazz.getName());
StringWriter sw = new StringWriter();
classReader.accept(new TraceClassVisitor(new PrintWriter(sw)), 0);
return sw.toString();
}

public static String dumpBytecode(byte[] bytecode) {
ClassReader classReader = new ClassReader(bytecode);
StringWriter sw = new StringWriter();
classReader.accept(new TraceClassVisitor(new PrintWriter(sw)), 0);
return sw.toString();
}

public static String dumpAsmSource(Class<?> clazz) throws Exception {
ClassReader classReader = new ClassReader(clazz.getName());
StringWriter sw = new StringWriter();
classReader.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(sw)), 0);
return sw.toString();
}

public static String dumpAsmSource(byte[] bytecode) {
ClassReader classReader = new ClassReader(bytecode);
StringWriter sw = new StringWriter();
classReader.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(sw)), 0);
return sw.toString();
}

public static String decompile(Class<?> clazz) throws Exception {
String resourceName = clazz.getName().replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader().getResourceAsStream(resourceName);
if (stream == null) {
throw new IllegalArgumentException("Cannot find class file for " + clazz.getName());
}
return decompile(stream.readAllBytes());
}

public static String decompile(byte[] bytecode) throws Exception {
Path tempDir = Files.createTempDirectory("fernflower");
Path outputDir = Files.createTempDirectory("fernflower-output");
try {
Path classFile = tempDir.resolve("TempClass.class");
Files.write(classFile, bytecode);

FernflowerDecompiler decompiler = new FernflowerDecompiler();
decompiler.decompileClassFile(tempDir, classFile, outputDir);

Path decompiledFile = outputDir.resolve("TempClass.java");
if (Files.exists(decompiledFile)) {
return Files.readString(decompiledFile);
}
return Files.walk(outputDir)
.filter(p -> p.toString().endsWith(".java"))
.findFirst()
.map(p -> {
try {
return Files.readString(p);
} catch (Exception e) {
return "// Decompilation failed";
}
})
.orElse("// Decompilation failed");
} finally {
deleteRecursively(tempDir);
deleteRecursively(outputDir);
}
}

private static void deleteRecursively(Path dir) {
if (!Files.exists(dir))
return;
try {
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.forEach(p -> {
try {
Files.delete(p);
} catch (Exception ignored) {
}
});
} catch (Exception ignored) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.morphia.critter.parser;

import dev.morphia.mapping.codec.pojo.EntityModel;

public class BaseCritterTest {
protected EntityModel exampleEntityModel = new EntityModel(String.class);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package dev.morphia.critter.parser;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import dev.morphia.critter.ClassfileOutput;
import dev.morphia.critter.CritterClassLoader;
import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator;
import dev.morphia.critter.parser.gizmo.GizmoEntityModelGenerator;
import dev.morphia.critter.sources.Example;
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;

import io.github.classgraph.ClassGraph;

public class GeneratorTest {
public static final CritterEntityModel entityModel;
public static final CritterClassLoader critterClassLoader = new CritterClassLoader();

static {
ClassGraph classGraph = new ClassGraph()
.addClassLoader(critterClassLoader)
.enableAllInfo();
classGraph.acceptPackages("dev.morphia.critter.sources");

try (var scanResult = classGraph.scan()) {
for (var classInfo : scanResult.getAllClasses()) {
try {
ClassfileOutput.dump(critterClassLoader, classInfo.getName());
} catch (Throwable ignored) {
}
}
} catch (Exception ignored) {
}

GizmoEntityModelGenerator gen = CritterGizmoGenerator.INSTANCE.generate(Example.class, critterClassLoader, false);
try {
entityModel = (CritterEntityModel) critterClassLoader
.loadClass(gen.getGeneratedType())
.getConstructors()[0]
.newInstance(Generators.INSTANCE.getMapper());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Object[][] methodNames(Class<?> clazz) {
return methods(clazz).stream()
.map(m -> new Object[] { m.getName(), m })
.sorted(Comparator.comparing(a -> a[0].toString()))
.toArray(Object[][]::new);
}

public static List<Method> methods(Class<?> clazz) {
return Arrays.stream(clazz.getMethods())
.filter(m -> !Modifier.isFinal(m.getModifiers()))
.filter(m -> m.getParameterCount() == 0)
.filter(m -> m.getDeclaringClass() == clazz)
.collect(Collectors.toList());
}

/** Helper: remove list elements while predicate holds, then remove one more, return joined string. */
static String removeWhile(List<String> list, Predicate<String> predicate) {
List<String> removed = new ArrayList<>();
while (!list.isEmpty() && predicate.test(list.get(0))) {
removed.add(list.remove(0));
}
if (!list.isEmpty()) {
removed.add(list.remove(0));
}
return String.join("\n", removed);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dev.morphia.critter.parser;

import java.util.List;

import dev.morphia.critter.Critter;
import dev.morphia.critter.CritterClassLoader;
import dev.morphia.critter.CritterKt;
import dev.morphia.critter.sources.Example;

import org.bson.codecs.pojo.PropertyAccessor;
import org.testng.Assert;
import org.testng.annotations.DataProvider;

public class TestAccessorsMutators extends BaseCritterTest {
private final CritterClassLoader critterClassLoader = new CritterClassLoader();

// @Test(dataProvider = "classes")
public void testPropertyAccessors(Class<?> type) throws Exception {
List<List<Object>> testFields = List.of(
List.of("name", String.class, "set externally"),
List.of("age", int.class, 100),
List.of("salary", Long.class, 100_000L));

Object entity = critterClassLoader.loadClass(type.getName()).getConstructor().newInstance();

for (List<Object> field : testFields) {
testAccessor(type, critterClassLoader, entity, (String) field.get(0), field.get(2));
}
}

@SuppressWarnings("unchecked")
private void testAccessor(
Class<?> type,
CritterClassLoader loader,
Object entity,
String fieldName,
Object testValue) throws Exception {
Class<PropertyAccessor<Object>> accessorClass = (Class<PropertyAccessor<Object>>) loader.loadClass(
Critter.Companion.critterPackage(type)
+ type.getSimpleName()
+ CritterKt.titleCase(fieldName)
+ "Accessor");
PropertyAccessor<Object> accessor = accessorClass.getConstructor().newInstance();

accessor.set(entity, testValue);
Assert.assertEquals(accessor.get(entity), testValue);
Assert.assertTrue(
entity.toString().contains(testValue.toString()),
"Could not find '" + testValue + "` in :" + entity);
}

@DataProvider(name = "classes")
public Object[][] names() {
return new Object[][] { { Example.class } };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.morphia.critter.parser;

import java.lang.reflect.Method;

import dev.morphia.critter.ClassfileOutput;
import dev.morphia.critter.CritterClassLoader;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.ReflectiveMapper;
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.NoInjection;

public class TestEntityModelGenerator {
private static final Logger LOG = LoggerFactory.getLogger(TestEntityModelGenerator.class);

private final CritterEntityModel control;
private final Mapper mapper = new ReflectiveMapper(Generators.INSTANCE.getConfig());
private final CritterClassLoader critterClassLoader = new CritterClassLoader();

public TestEntityModelGenerator() {
CritterEntityModel tmp;
try {
tmp = (CritterEntityModel) critterClassLoader
.loadClass("dev.morphia.critter.sources.ExampleEntityModelTemplate")
.getConstructor(Mapper.class)
.newInstance(mapper);
ClassfileOutput.dump(critterClassLoader, "dev.morphia.critter.sources.ExampleEntityModelTemplate");
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new RuntimeException(e);
}
control = tmp;
}

// @Test(dataProvider = "methods")
public void testEntityModel(String name, @NoInjection Method method) throws Exception {
Object expected = method.invoke(control);
Object actual = method.invoke(GeneratorTest.entityModel);
Assert.assertEquals(actual, expected, method.getName() + " should return the same value");
}

@DataProvider(name = "methods")
public Object[][] methods() {
return GeneratorTest.methodNames(CritterEntityModel.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.morphia.critter.parser;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import dev.morphia.critter.CritterClassLoader;
import dev.morphia.mapping.codec.pojo.EntityModel;
import dev.morphia.mapping.codec.pojo.PropertyModel;
import dev.morphia.mapping.codec.pojo.critter.CritterPropertyModel;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.NoInjection;

public class TestPropertyModelGenerator extends BaseCritterTest {
private final CritterClassLoader critterClassLoader = new CritterClassLoader();

// @Test(dataProvider = "properties", testName = "")
public void testProperty(String control, String methodName, @NoInjection Method method) throws Exception {
CritterPropertyModel propertyModel = getModel(control);
System.out.println("exampleModel = [" + control + "], methodName = [" + methodName + "], method = [" + method + "]");
Object expected = method.invoke(control);
Object actual = method.invoke(propertyModel);
Assert.assertEquals(actual, expected, method.getName() + " should return the same value");
}

private CritterPropertyModel getModel(String name) {
return (CritterPropertyModel) GeneratorTest.entityModel.getProperty(name);
}

@DataProvider(name = "properties")
public Object[][] methods() {
Object[][] methods = GeneratorTest.methodNames(CritterPropertyModel.class);
return List.of("dev.morphia.critter.sources.ExampleNamePropertyModelTemplate").stream()
.map(type -> {
try {
return loadModel(type);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.flatMap(propertyModel -> Arrays.stream(methods)
.map(method -> new Object[] { propertyModel.getName(), method[0], method[1] }))
.toArray(Object[][]::new);
}

private PropertyModel loadModel(String type) throws Exception {
return (PropertyModel) critterClassLoader
.loadClass(type)
.getConstructor(EntityModel.class)
.newInstance(GeneratorTest.entityModel);
}
}
Loading
Loading