From b371e9a254165e07021de415628c36758e63384f Mon Sep 17 00:00:00 2001 From: evanchooly Date: Sat, 28 Feb 2026 23:00:35 -0500 Subject: [PATCH] Phase 3.1: Convert critter-core test sources from Kotlin to Java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces all 9 Kotlin test files under critter/core/src/test/kotlin/ with equivalent Java implementations. The Kotlin production code is unchanged; all tests compile and pass against it (57 tests, 0 failures). Converted files: - ClassfileOutput.kt → ClassfileOutput.java - parser/BaseCritterTest.kt → parser/BaseCritterTest.java - parser/GeneratorTest.kt → parser/GeneratorTest.java - parser/TestAccessorsMutators.kt → parser/TestAccessorsMutators.java - parser/TestEntityModelGenerator.kt → parser/TestEntityModelGenerator.java - parser/TestPropertyModelGenerator.kt → parser/TestPropertyModelGenerator.java - parser/TestVarHandleAccessor.kt → parser/TestVarHandleAccessor.java - parser/TypesTest.kt → parser/TypesTest.java - parser/gizmo/TestGizmoGeneration.kt → parser/gizmo/TestGizmoGeneration.java Key conversion patterns applied: - Kotlin object singletons → Java static classes - Extension functions → static helper methods - Kotlin object API → INSTANCE access (Generators.INSTANCE, CritterGizmoGenerator.INSTANCE) - Kotlin top-level funs → *Kt class static methods (CritterKt, PropertyModelGeneratorKt, GizmoExtensionsKt) - Companion object methods → Companion access (Critter.Companion.critterPackage) - scan().use {} → try-with-resources - Lambda collections → Java streams Closes task 3.1 of Phase 3 (#4195). Co-Authored-By: Claude Sonnet 4.6 --- .../dev/morphia/critter/ClassfileOutput.java | 142 +++++++ .../critter/parser/BaseCritterTest.java | 7 + .../morphia/critter/parser/GeneratorTest.java | 78 ++++ .../critter/parser/TestAccessorsMutators.java | 56 +++ .../parser/TestEntityModelGenerator.java | 50 +++ .../parser/TestPropertyModelGenerator.java | 54 +++ .../critter/parser/TestVarHandleAccessor.java | 91 ++++ .../dev/morphia/critter/parser/TypesTest.java | 87 ++++ .../parser/gizmo/TestGizmoGeneration.java | 325 ++++++++++++++ .../dev/morphia/critter/ClassfileOutput.kt | 154 ------- .../morphia/critter/parser/BaseCritterTest.kt | 7 - .../morphia/critter/parser/GeneratorTest.kt | 94 ---- .../critter/parser/TestAccessorsMutators.kt | 59 --- .../parser/TestEntityModelGenerator.kt | 57 --- .../parser/TestPropertyModelGenerator.kt | 48 --- .../critter/parser/TestVarHandleAccessor.kt | 89 ---- .../dev/morphia/critter/parser/TypesTest.kt | 87 ---- .../parser/gizmo/TestGizmoGeneration.kt | 402 ------------------ 18 files changed, 890 insertions(+), 997 deletions(-) create mode 100644 critter/core/src/test/java/dev/morphia/critter/ClassfileOutput.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/BaseCritterTest.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/GeneratorTest.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/TestAccessorsMutators.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/TestEntityModelGenerator.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/TestPropertyModelGenerator.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/TestVarHandleAccessor.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/TypesTest.java create mode 100644 critter/core/src/test/java/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.java delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/ClassfileOutput.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/BaseCritterTest.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/GeneratorTest.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/TestAccessorsMutators.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/TestEntityModelGenerator.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/TestPropertyModelGenerator.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/TestVarHandleAccessor.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/TypesTest.kt delete mode 100644 critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt diff --git a/critter/core/src/test/java/dev/morphia/critter/ClassfileOutput.java b/critter/core/src/test/java/dev/morphia/critter/ClassfileOutput.java new file mode 100644 index 00000000000..2432d142ba0 --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/ClassfileOutput.java @@ -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) { + } + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/BaseCritterTest.java b/critter/core/src/test/java/dev/morphia/critter/parser/BaseCritterTest.java new file mode 100644 index 00000000000..252bf226bcd --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/BaseCritterTest.java @@ -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); +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/GeneratorTest.java b/critter/core/src/test/java/dev/morphia/critter/parser/GeneratorTest.java new file mode 100644 index 00000000000..846fbabbd5a --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/GeneratorTest.java @@ -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 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 list, Predicate predicate) { + List 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); + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/TestAccessorsMutators.java b/critter/core/src/test/java/dev/morphia/critter/parser/TestAccessorsMutators.java new file mode 100644 index 00000000000..84b52130e3d --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/TestAccessorsMutators.java @@ -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> 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 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> accessorClass = (Class>) loader.loadClass( + Critter.Companion.critterPackage(type) + + type.getSimpleName() + + CritterKt.titleCase(fieldName) + + "Accessor"); + PropertyAccessor 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 } }; + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/TestEntityModelGenerator.java b/critter/core/src/test/java/dev/morphia/critter/parser/TestEntityModelGenerator.java new file mode 100644 index 00000000000..2f56301ea7b --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/TestEntityModelGenerator.java @@ -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); + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/TestPropertyModelGenerator.java b/critter/core/src/test/java/dev/morphia/critter/parser/TestPropertyModelGenerator.java new file mode 100644 index 00000000000..f6b99c4d4dc --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/TestPropertyModelGenerator.java @@ -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); + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/TestVarHandleAccessor.java b/critter/core/src/test/java/dev/morphia/critter/parser/TestVarHandleAccessor.java new file mode 100644 index 00000000000..6e46479428d --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/TestVarHandleAccessor.java @@ -0,0 +1,91 @@ +package dev.morphia.critter.parser; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import dev.morphia.critter.Critter; +import dev.morphia.critter.CritterClassLoader; +import dev.morphia.critter.CritterKt; +import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator; +import dev.morphia.critter.sources.Example; + +import org.bson.codecs.pojo.PropertyAccessor; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class TestVarHandleAccessor { + private CritterClassLoader classLoader; + + @BeforeClass + public void setup() { + classLoader = new CritterClassLoader(); + CritterGizmoGenerator.INSTANCE.generate(Example.class, classLoader, true); + } + + @Test + public void testEntityNotModified() { + List methods = Arrays.stream(Example.class.getDeclaredMethods()) + .map(m -> m.getName()) + .collect(Collectors.toList()); + + List syntheticRead = methods.stream() + .filter(name -> name.startsWith("__read") && !name.endsWith("Template")) + .collect(Collectors.toList()); + List syntheticWrite = methods.stream() + .filter(name -> name.startsWith("__write") && !name.endsWith("Template")) + .collect(Collectors.toList()); + + Assert.assertTrue(syntheticRead.isEmpty(), + "Entity class should not have synthetic __read methods but found: " + syntheticRead); + Assert.assertTrue(syntheticWrite.isEmpty(), + "Entity class should not have synthetic __write methods but found: " + syntheticWrite); + } + + @Test + public void testStringField() throws Exception { + Example entity = new Example(); + PropertyAccessor accessor = loadAccessor(Example.class, "name"); + + Assert.assertNull(accessor.get(entity)); + accessor.set(entity, "hello"); + Assert.assertEquals(accessor.get(entity), "hello"); + } + + @Test + public void testIntPrimitiveField() throws Exception { + Example entity = new Example(); + PropertyAccessor accessor = loadAccessor(Example.class, "age"); + + Assert.assertEquals(accessor.get(entity), 21); + accessor.set(entity, 42); + Assert.assertEquals(accessor.get(entity), 42); + } + + @Test + public void testLongBoxedField() throws Exception { + Example entity = new Example(); + PropertyAccessor accessor = loadAccessor(Example.class, "salary"); + + Assert.assertEquals(accessor.get(entity), 2L); + accessor.set(entity, 100_000L); + Assert.assertEquals(accessor.get(entity), 100_000L); + } + + @Test + public void testAccessorsInstantiatable() throws Exception { + for (String field : List.of("name", "age", "salary")) { + Class cls = classLoader.loadClass( + Critter.Companion.critterPackage(Example.class) + "." + CritterKt.titleCase(field) + "Accessor"); + cls.getConstructor().newInstance(); + } + } + + @SuppressWarnings("unchecked") + private PropertyAccessor loadAccessor(Class entityType, String fieldName) throws Exception { + Class> cls = (Class>) classLoader.loadClass( + Critter.Companion.critterPackage(entityType) + "." + CritterKt.titleCase(fieldName) + "Accessor"); + return cls.getConstructor().newInstance(); + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/TypesTest.java b/critter/core/src/test/java/dev/morphia/critter/parser/TypesTest.java new file mode 100644 index 00000000000..58d991c1951 --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/TypesTest.java @@ -0,0 +1,87 @@ +package dev.morphia.critter.parser; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +import org.objectweb.asm.Type; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class TypesTest { + + @DataProvider(name = "types") + public Object[][] typeProvider() { + return new Object[][] { + // Primitive types + { boolean.class }, + { char.class }, + { byte.class }, + { short.class }, + { int.class }, + { float.class }, + { long.class }, + { double.class }, + + // Object types + { String.class }, + { Locale.class }, + { Date.class }, + { UUID.class }, + { BigDecimal.class }, + { Instant.class }, + + // Arrays of boxed primitives (Kotlin Array = Java boxed X[]) + { Boolean[].class }, + { Character[].class }, + { Byte[].class }, + { Short[].class }, + { Integer[].class }, + { Float[].class }, + { Long[].class }, + { Double[].class }, + + // Primitive arrays (Kotlin XArray = Java primitive x[]) + { boolean[].class }, + { char[].class }, + { byte[].class }, + { short[].class }, + { int[].class }, + { float[].class }, + { long[].class }, + { double[].class }, + + // 2D primitive arrays (Kotlin Array = Java primitive x[][]) + { boolean[][].class }, + { int[][].class }, + + // Arrays of objects + { String[].class }, + { Locale[].class }, + { Date[].class }, + { UUID[].class }, + { BigDecimal[].class }, + { Instant[].class }, + + // Arrays of arrays (2D) + { Boolean[][].class }, + { Integer[][].class }, + { String[][].class }, + { Locale[][].class }, + + // Arrays of arrays of arrays (3D) + { Integer[][][].class }, + { String[][][].class }, + }; + } + + @Test(dataProvider = "types") + public void asClassConversion(Class expected) { + Type type = Type.getType(expected); + Class actual = Generators.INSTANCE.asClass(type, Thread.currentThread().getContextClassLoader()); + Assert.assertEquals(actual, expected, "Type " + type.getDescriptor() + " should convert to " + expected.getName()); + } +} diff --git a/critter/core/src/test/java/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.java b/critter/core/src/test/java/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.java new file mode 100644 index 00000000000..1523f93af97 --- /dev/null +++ b/critter/core/src/test/java/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.java @@ -0,0 +1,325 @@ +package dev.morphia.critter.parser.gizmo; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.EntityListeners; +import dev.morphia.annotations.Indexes; +import dev.morphia.annotations.internal.CollationBuilder; +import dev.morphia.annotations.internal.EntityBuilder; +import dev.morphia.annotations.internal.EntityListenersBuilder; +import dev.morphia.annotations.internal.FieldBuilder; +import dev.morphia.annotations.internal.IndexBuilder; +import dev.morphia.annotations.internal.IndexOptionsBuilder; +import dev.morphia.annotations.internal.IndexesBuilder; +import dev.morphia.critter.ClassfileOutput; +import dev.morphia.critter.CritterClassLoader; +import dev.morphia.critter.parser.Generators; +import dev.morphia.critter.parser.asm.AddMethodAccessorMethods; +import dev.morphia.critter.sources.Example; +import dev.morphia.critter.sources.MethodExample; +import dev.morphia.mapping.codec.pojo.EntityModel; +import dev.morphia.mapping.codec.pojo.PropertyModel; +import dev.morphia.mapping.codec.pojo.TypeData; +import dev.morphia.mapping.lifecycle.EntityListenerAdapter; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.MethodDescriptor; + +import static com.mongodb.client.model.CollationCaseFirst.LOWER; +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +public class TestGizmoGeneration { + private final CritterClassLoader critterClassLoader = new CritterClassLoader(); + + @Test + public void testMapStringExample() { + String descString = "Ljava/util/Map;"; + String descriptor = descriptor( + java.util.Map.class, + descriptor(String.class), + descriptor(Example.class)); + + Assert.assertEquals(descriptor, descString); + TypeData typeData = PropertyModelGeneratorKt.typeData(descString, Thread.currentThread().getContextClassLoader()).get(0); + Assert.assertEquals( + typeData, + typeDataHelper(java.util.Map.class, typeDataHelper(String.class), typeDataHelper(Example.class))); + } + + @Test + public void testListMapStringExample() { + String descString = "Ljava/util/List;>;"; + String descriptor = descriptor( + java.util.List.class, + descriptor( + java.util.Map.class, + descriptor(String.class), + descriptor(Example.class))); + Assert.assertEquals(descriptor, descString); + + TypeData typeData = PropertyModelGeneratorKt.typeData(descString, Thread.currentThread().getContextClassLoader()).get(0); + Assert.assertEquals( + typeData, + typeDataHelper(java.util.List.class, + typeDataHelper(java.util.Map.class, typeDataHelper(String.class), typeDataHelper(Example.class)))); + } + + @Test + public void testMapOfList() { + String descString = "Ljava/util/Map;>;"; + String descriptor = descriptor( + java.util.Map.class, + descriptor(String.class), + descriptor(java.util.List.class, descriptor(Example.class))); + Assert.assertEquals(descriptor, descString); + TypeData typeData = PropertyModelGeneratorKt.typeData(descriptor, Thread.currentThread().getContextClassLoader()).get(0); + Assert.assertEquals( + typeData, + typeDataHelper(java.util.Map.class, + typeDataHelper(String.class), + typeDataHelper(java.util.List.class, typeDataHelper(Example.class)))); + } + + @Test + public void testPrimitiveArray() { + TypeData typeData = PropertyModelGeneratorKt.typeData("[I", Thread.currentThread().getContextClassLoader()).get(0); + Assert.assertTrue(typeData.isArray()); + } + + @Test + public void testAnnotationBuilding() throws Exception { + AnnotationNode index = new AnnotationNode("Ldev/morphia/annotations/Index;"); + AnnotationNode field = new AnnotationNode("Ldev/morphia/annotations/Field;"); + index.values = List.of("fields", List.of(field)); + + try (ClassCreator creator = ClassCreator.builder() + .className("critter.AnnotationTest") + .superClass(EntityModel.class) + .classOutput((name, data) -> { + String className = name.replace('/', '.'); + critterClassLoader.register(className, data); + try { + ClassfileOutput.dump(name, data); + } catch (Exception ignored) { + } + }) + .build()) { + var mc = creator.getMethodCreator("test", Void.class); + MethodDescriptor annotationMethod = ofMethod( + EntityModel.class.getName(), + "annotation", + EntityModel.class.getName(), + java.lang.annotation.Annotation.class); + mc.invokeVirtualMethod( + annotationMethod, + mc.getThis(), + GizmoExtensionsKt.annotationBuilder(index, mc)); + } + } + + @Test + public void testGizmo() throws Exception { + CritterGizmoGenerator.INSTANCE.generate(Example.class, critterClassLoader, false); + critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.AgeModel"); + Class nameModel = critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.NameModel"); + invokeAll(PropertyModel.class, nameModel); + critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.SalaryModel"); + critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.AgeAccessor").getConstructor().newInstance(); + critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.NameAccessor").getConstructor().newInstance(); + critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.SalaryAccessor").getConstructor().newInstance(); + + Class loadClass = critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.ExampleEntityModel"); + EntityModel model = (EntityModel) loadClass.getConstructors()[0].newInstance(Generators.INSTANCE.getMapper()); + validate(model); + } + + private void validate(EntityModel model) { + Assert.assertEquals( + model.getAnnotation(EntityListeners.class), + EntityListenersBuilder.entityListenersBuilder().value(EntityListenerAdapter.class).build()); + Assert.assertEquals( + model.getAnnotation(Entity.class), + EntityBuilder.entityBuilder().value("examples").build()); + Assert.assertEquals( + model.getAnnotation(Indexes.class), + IndexesBuilder.indexesBuilder() + .value(IndexBuilder.indexBuilder() + .fields(FieldBuilder.fieldBuilder().value("name").weight(42).build()) + .options(IndexOptionsBuilder.indexOptionsBuilder() + .partialFilter("partial filter") + .collation(CollationBuilder.collationBuilder().caseFirst(LOWER).build()) + .build()) + .build()) + .build()); + Assert.assertEquals(model.collectionName(), "examples"); + Assert.assertEquals(model.discriminator(), "Example"); + Assert.assertEquals(model.discriminatorKey(), "_t"); + Assert.assertEquals(model.getType().getName(), Example.class.getName()); + Assert.assertFalse(model.getProperties().isEmpty(), "Should have properties"); + Assert.assertNotNull(model.getIdProperty(), "Should have an ID property"); + Assert.assertFalse(model.isAbstract(), "Should not be abstract"); + Assert.assertFalse(model.isInterface(), "Should not be an interface"); + Assert.assertTrue(model.useDiscriminator(), "Should use the discriminator"); + Assert.assertTrue(model.classHierarchy().isEmpty(), "Should not have a class hierarchy"); + } + + private void invokeAll(Class type, Class klass) { + Object instance; + try { + instance = klass.getConstructors()[0].newInstance(new Object[] { null }); + } catch (Exception e) { + Assert.fail("Could not instantiate " + klass.getName() + ": " + e.getMessage()); + return; + } + List results = Arrays.stream(type.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers()) + && !Modifier.isFinal(m.getModifiers()) + && m.getParameterCount() == 0) + .filter(m -> !List.of("hashCode", "toString").contains(m.getName())) + .sorted(Comparator.comparing(Method::getName)) + .map(method -> { + try { + klass.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return null; + } catch (Exception e) { + return e.getMessage(); + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (!results.isEmpty()) { + Assert.fail("Missing methods from " + type.getName() + ": \n" + String.join("\n", results)); + } + } + + @Test + public void testConstructors() throws Exception { + String className = "dev.morphia.critter.GizmoSubclass"; + + try (ClassCreator constructorCall = ClassCreator.builder() + .classOutput((name, data) -> critterClassLoader.register(name.replace('/', '.'), data)) + .className("dev.morphia.critter.ConstructorCall") + .build()) { + var fieldCreator = constructorCall.getFieldCreator("name", String.class) + .setModifiers(Modifier.PUBLIC); + var constructorCreator = constructorCall.getConstructorCreator(String.class); + constructorCreator.invokeSpecialMethod( + MethodDescriptor.ofConstructor(Object.class), + constructorCreator.getThis()); + constructorCreator.setParameterNames(new String[] { "name" }); + constructorCreator.writeInstanceField( + fieldCreator.getFieldDescriptor(), + constructorCreator.getThis(), + constructorCreator.getMethodParam(0)); + constructorCreator.returnVoid(); + } + + critterClassLoader.loadClass("dev.morphia.critter.ConstructorCall") + .getConstructor(String.class) + .newInstance("here i am"); + + try (ClassCreator creator = ClassCreator.builder() + .classOutput((name, data) -> critterClassLoader.register(name.replace('/', '.'), data)) + .className(className) + .superClass("dev.morphia.critter.ConstructorCall") + .build()) { + var constructor = creator.getConstructorCreator(String.class); + constructor.invokeSpecialMethod( + MethodDescriptor.ofConstructor("dev.morphia.critter.ConstructorCall", String.class), + constructor.getThis(), + constructor.getMethodParam(0)); + constructor.setParameterNames(new String[] { "subName" }); + constructor.returnVoid(); + } + + Object instance = critterClassLoader.loadClass(className) + .getConstructor(String.class) + .newInstance("This is my name"); + Assert.assertNotNull(instance); + } + + @Test + public void testMethodBasedAccessors() throws Exception { + CritterClassLoader classLoader = new CritterClassLoader(); + + String resourceName = MethodExample.class.getName().replace('.', '/') + ".class"; + var inputStream = MethodExample.class.getClassLoader().getResourceAsStream(resourceName); + ClassNode classNode = new ClassNode(); + new ClassReader(inputStream).accept(classNode, 0); + + List methodNodes = classNode.methods.stream() + .filter(node -> node.name.startsWith("get")) + .filter(node -> Type.getArgumentTypes(node.desc).length == 0) + .filter(node -> node.visibleAnnotations != null + && node.visibleAnnotations.stream().anyMatch( + ann -> List.of("Ldev/morphia/annotations/Id;", "Ldev/morphia/annotations/Property;").contains(ann.desc))) + .collect(Collectors.toList()); + + List methodNames = methodNodes.stream().map(n -> n.name).collect(Collectors.toList()); + Assert.assertTrue(methodNames.contains("getId"), "Should find getId method"); + Assert.assertTrue(methodNames.contains("getCount"), "Should find getCount method"); + Assert.assertTrue(methodNames.contains("getScore"), "Should find getScore method"); + Assert.assertTrue(methodNames.contains("getComputedValue"), "Should find getComputedValue method"); + Assert.assertEquals(4, methodNodes.size(), "Should find exactly 4 annotated getter methods"); + + byte[] bytecode = new AddMethodAccessorMethods(MethodExample.class, methodNodes).emit(); + + classLoader.register(MethodExample.class.getName(), bytecode); + Class modifiedClass = classLoader.loadClass(MethodExample.class.getName()); + + Assert.assertNotNull(modifiedClass.getMethod("__readId"), "Should have __readId method"); + Assert.assertNotNull(modifiedClass.getMethod("__readCount"), "Should have __readCount method"); + Assert.assertNotNull(modifiedClass.getMethod("__readScore"), "Should have __readScore method"); + Assert.assertNotNull(modifiedClass.getMethod("__readComputedValue"), "Should have __readComputedValue method"); + + Assert.assertNotNull(modifiedClass.getMethod("__writeId", org.bson.types.ObjectId.class), "Should have __writeId method"); + Assert.assertNotNull(modifiedClass.getMethod("__writeCount", long.class), "Should have __writeCount method"); + Assert.assertNotNull(modifiedClass.getMethod("__writeScore", double.class), "Should have __writeScore method"); + + Object instance = modifiedClass.getConstructor().newInstance(); + Method writeComputedMethod = modifiedClass.getMethod("__writeComputedValue", String.class); + + try { + writeComputedMethod.invoke(instance, "test value"); + Assert.fail("Should throw UnsupportedOperationException for read-only property"); + } catch (InvocationTargetException e) { + Assert.assertTrue(e.getCause() instanceof UnsupportedOperationException, + "Should throw UnsupportedOperationException, got: " + e.getCause()); + Assert.assertTrue( + e.getCause().getMessage() != null && e.getCause().getMessage().contains("read-only"), + "Exception message should mention read-only"); + } + } + + private String descriptor(Class type, String... typeParameters) { + String desc = Type.getDescriptor(type); + if (typeParameters.length > 0) { + desc = desc.substring(0, desc.length() - 1) + + "<" + String.join("", typeParameters) + ">" + + ";"; + } + return desc; + } + + @SuppressWarnings("unchecked") + private static TypeData typeDataHelper(Class clazz, TypeData... params) { + return new TypeData<>(clazz, Arrays.asList(params)); + } +} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/ClassfileOutput.kt b/critter/core/src/test/kotlin/dev/morphia/critter/ClassfileOutput.kt deleted file mode 100644 index 61e7208ab6c..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/ClassfileOutput.kt +++ /dev/null @@ -1,154 +0,0 @@ -package dev.morphia.critter - -import java.io.PrintWriter -import java.io.StringWriter -import java.nio.file.Files -import java.nio.file.Path -import kotlin.io.path.readText -import kotlin.io.path.writeText -import org.jboss.windup.decompiler.fernflower.FernflowerDecompiler -import org.objectweb.asm.ClassReader -import org.objectweb.asm.util.ASMifier -import org.objectweb.asm.util.TraceClassVisitor - -object ClassfileOutput { - fun CritterClassLoader.dump(className: String, outputDir: Path = Path.of("target/dumps/")) { - val bytes = bytes(className) - listOf( - "javap" to dumpBytecode(bytes), - "asm" to dumpAsmSource(bytes), - "java" to decompile(bytes), - ) - .forEach { (ext, text) -> - val output = outputDir.resolve("${className.replace('.', '/')}.$ext") - output.toFile().parentFile.mkdirs() - output.writeText(text) - } - } - - fun dump(className: String, bytes: ByteArray, outputDir: Path = Path.of("target/dumps/")) { - listOf( - "javap" to dumpBytecode(bytes), - "asm" to dumpAsmSource(bytes), - "java" to decompile(bytes), - ) - .forEach { (ext, text) -> - val output = outputDir.resolve("$className.$ext") - output.toFile().parentFile.mkdirs() - output.writeText(text) - } - } - - /** - * Dumps the bytecode of a given class as human-readable text using ASM's TraceClassVisitor. - * - * @param clazz the class to dump - * @return a string containing the textual representation of the class bytecode - */ - fun dumpBytecode(clazz: Class<*>): String { - val classReader = ClassReader(clazz.name) - val stringWriter = StringWriter() - val printWriter = PrintWriter(stringWriter) - val traceClassVisitor = TraceClassVisitor(printWriter) - classReader.accept(traceClassVisitor, 0) - return stringWriter.toString() - } - - /** - * Dumps the bytecode from a byte array as human-readable text using ASM's TraceClassVisitor. - * - * @param bytecode the raw bytecode to dump - * @return a string containing the textual representation of the class bytecode - */ - fun dumpBytecode(bytecode: ByteArray): String { - val classReader = ClassReader(bytecode) - val stringWriter = StringWriter() - val printWriter = PrintWriter(stringWriter) - val traceClassVisitor = TraceClassVisitor(printWriter) - classReader.accept(traceClassVisitor, 0) - return stringWriter.toString() - } - - /** - * Dumps the ASM source code that would generate the given class using ASM's ASMifier. This - * produces Java source code that uses the ASM API to recreate the class. - * - * @param clazz the class to dump - * @return a string containing the ASM API source code - */ - fun dumpAsmSource(clazz: Class<*>): String { - val classReader = ClassReader(clazz.name) - val stringWriter = StringWriter() - val printWriter = PrintWriter(stringWriter) - val traceClassVisitor = TraceClassVisitor(null, ASMifier(), printWriter) - classReader.accept(traceClassVisitor, 0) - return stringWriter.toString() - } - - /** - * Dumps the ASM source code that would generate the class from a byte array using ASM's - * ASMifier. This produces Java source code that uses the ASM API to recreate the class. - * - * @param bytecode the raw bytecode to dump - * @return a string containing the ASM API source code - */ - fun dumpAsmSource(bytecode: ByteArray): String { - val classReader = ClassReader(bytecode) - val stringWriter = StringWriter() - val printWriter = PrintWriter(stringWriter) - val traceClassVisitor = TraceClassVisitor(null, ASMifier(), printWriter) - classReader.accept(traceClassVisitor, 0) - return stringWriter.toString() - } - - /** - * Decompiles the given class to Java source code using Fernflower. - * - * @param clazz the class to decompile - * @return a string containing the decompiled Java source code - */ - fun decompile(clazz: Class<*>): String { - val className = clazz.name.replace('.', '/') + ".class" - val bytecode = - clazz.classLoader.getResourceAsStream(className)?.readBytes() - ?: throw IllegalArgumentException("Cannot find class file for ${clazz.name}") - return decompile(bytecode) - } - - /** - * Decompiles bytecode to Java source code using Fernflower. - * - * @param bytecode the raw bytecode to decompile - * @return a string containing the decompiled Java source code - */ - fun decompile(bytecode: ByteArray): String { - val tempDir = Files.createTempDirectory("fernflower") - val outputDir = Files.createTempDirectory("fernflower-output") - - try { - // Write bytecode to a temporary class file - val classFile = tempDir.resolve("TempClass.class") - Files.write(classFile, bytecode) - - // Decompile using FernflowerDecompiler - val decompiler = FernflowerDecompiler() - decompiler.decompileClassFile(tempDir, classFile, outputDir) - - // Read the decompiled source - val decompiledFile = outputDir.resolve("TempClass.java") - return if (Files.exists(decompiledFile)) { - decompiledFile.readText() - } else { - // If exact name doesn't exist, find any .java file in output - Files.walk(outputDir) - .filter { it.toString().endsWith(".java") } - .findFirst() - .map { it.readText() } - .orElse("// Decompilation failed") - } - } finally { - tempDir.toFile().deleteRecursively() - outputDir.toFile().deleteRecursively() - } - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/BaseCritterTest.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/BaseCritterTest.kt deleted file mode 100644 index 30165117d66..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/BaseCritterTest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.mapping.codec.pojo.EntityModel - -open class BaseCritterTest { - var exampleEntityModel = EntityModel(String::class.java) // GeneratorTest.entityModel -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/GeneratorTest.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/GeneratorTest.kt deleted file mode 100644 index e4130c59413..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/GeneratorTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.ClassfileOutput.dump -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.parser.Generators.mapper -import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator as generator -import dev.morphia.critter.parser.java.CritterParser.asmify -import dev.morphia.critter.sources.Example -import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel -import io.github.classgraph.ClassGraph -import java.lang.reflect.Modifier -import org.objectweb.asm.Type - -object GeneratorTest { - var entityModel: CritterEntityModel - val critterClassLoader = CritterClassLoader() - - init { - val classGraph = ClassGraph().addClassLoader(critterClassLoader).enableAllInfo() - classGraph.acceptPackages("dev.morphia.critter.sources") - - classGraph.scan().use { scanResult -> - for (classInfo in scanResult.allClasses) { - try { - val name = classInfo.name - critterClassLoader.dump(name) - } catch (_: Throwable) {} - } - } - val generator = generator.generate(Example::class.java, critterClassLoader) - - entityModel = - critterClassLoader - .loadClass(generator.generatedType) - .constructors[0] - .newInstance(mapper) as CritterEntityModel - } - - fun methodNames(clazz: Class<*>): Array> { - return methods(clazz) - .map { arrayOf(it.name, it) } - .sortedBy { it[0].toString() } - .toTypedArray() - } - - fun methods(clazz: Class<*>) = - clazz.methods - .filterNot { method -> Modifier.isFinal(method.modifiers) } - .filter { method -> method.parameterCount == 0 } - .filter { method -> method.declaringClass == clazz } - - fun process(resourceName: String, entity: Type, generated: Type): String { - var asm = asmify(critterClassLoader.getResourceAsStream(resourceName).readAllBytes()) - - val lines = asm.lines() - val imports = lines.filter { it.startsWith("import ") } - val pkg = - Regex("^(package )(?.*);\$").find(lines.first())!!.groups[2]!!.value + ".__morphia" - val body = - asm.substringAfter("{") - .substringBeforeLast("}") - .substringAfter("{") - .substringBeforeLast("}") - var header = body.substringBefore("{") - val methods = extractMethodDefinitions(body).map { bind(it) }.toMap(LinkedHashMap()) - return "" - // return "package ;"}\n" + - // lines.drop(1) - // .map { it.replace("dev/morphia/critter/sources/ExampleEntityModel", - // "generatedType.internalName") } - // .joinToString("\n") - //// .replace() - } - - private fun bind(methodBody: String): Pair { - fun extractMethodName(line: String): String { - return line.split("\"")[1] - } - - val lines = methodBody.lines().drop(1).dropLast(1) - - return extractMethodName(lines.first()) to lines.joinToString("\n") - } - - private fun extractMethodDefinitions(body: String): List { - val methodBody = body.substring(body.indexOf('{')).lines().toMutableList() - var methods = listOf() - while (methodBody.first().startsWith("{")) { - methods += methodBody.removeWhile { it.trim() != "}" } - } - - return methods - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestAccessorsMutators.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestAccessorsMutators.kt deleted file mode 100644 index 5ccc91b077c..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestAccessorsMutators.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.Critter.Companion.critterPackage -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.sources.Example -import dev.morphia.critter.titleCase -import org.bson.codecs.pojo.PropertyAccessor -import org.testng.Assert.assertEquals -import org.testng.Assert.assertTrue -import org.testng.annotations.DataProvider - -class TestAccessorsMutators : BaseCritterTest() { - val critterClassLoader = CritterClassLoader() - - // @Test(dataProvider = "classes") - fun testPropertyAccessors(type: Class<*>) { - val testFields = - listOf( - listOf("name", String::class.java, "set externally"), - listOf("age", Int::class.java, 100), - listOf("salary", java.lang.Long::class.java, 100_000L), - ) - - val entity = critterClassLoader.loadClass(type.name).getConstructor().newInstance() - - testFields.forEach { field -> - testAccessor(type, critterClassLoader, entity, field[0] as String, field[2]) - } - } - - @Suppress("UNCHECKED_CAST") - private fun testAccessor( - type: Class<*>, - critterClassLoader: CritterClassLoader, - entity: Any, - fieldName: String, - testValue: Any, - ) { - val accessor = - (critterClassLoader.loadClass( - "${critterPackage(type)}${type.simpleName}${fieldName.titleCase()}Accessor" - ) as Class>) - .getConstructor() - .newInstance() - - accessor.set(entity, testValue) - - assertEquals(accessor.get(entity), testValue) - assertTrue( - entity.toString().contains(testValue.toString()), - "Could not find '$testValue` in :${entity}", - ) - } - - @DataProvider(name = "classes") - fun names(): Array> { - return arrayOf(Example::class.java /*, KotlinDummyEntity::class.java*/) - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestEntityModelGenerator.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestEntityModelGenerator.kt deleted file mode 100644 index 0758fd45d6d..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestEntityModelGenerator.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.ClassfileOutput.dump -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.parser.GeneratorTest.entityModel -import dev.morphia.critter.parser.GeneratorTest.methodNames -import dev.morphia.mapping.Mapper -import dev.morphia.mapping.ReflectiveMapper -import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel -import java.lang.reflect.Method -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.testng.Assert.assertEquals -import org.testng.annotations.DataProvider -import org.testng.annotations.NoInjection - -class TestEntityModelGenerator { - companion object { - val LOG: Logger = LoggerFactory.getLogger(TestEntityModelGenerator::class.java) - } - - val control: CritterEntityModel - val mapper = ReflectiveMapper(Generators.config) - val critterClassLoader = CritterClassLoader() - - init { - try { - control = - critterClassLoader - .loadClass("dev.morphia.critter.sources.ExampleEntityModelTemplate") - .getConstructor(Mapper::class.java) - .newInstance(mapper) as CritterEntityModel - critterClassLoader.dump("dev.morphia.critter.sources.ExampleEntityModelTemplate") - } catch (e: Exception) { - LOG.error(e.message, e) - throw e - } - } - - // @Test(dataProvider = "methods") - fun testEntityModel(name: String, @NoInjection method: Method) { - val expected = method.invoke(control) - val actual = method.invoke(entityModel) - assertEquals(actual, expected, "${method.name} should return the same value") - } - - @DataProvider(name = "methods") fun methods() = methodNames(CritterEntityModel::class.java) -} - -fun MutableList.removeWhile(function: (String) -> Boolean): kotlin.String { - val removed = mutableListOf() - while (isNotEmpty() && function(first())) { - removed += removeFirst() - } - - return (removed + removeFirst()).joinToString("\n") -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestPropertyModelGenerator.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestPropertyModelGenerator.kt deleted file mode 100644 index 4e6695d0d01..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestPropertyModelGenerator.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.parser.GeneratorTest.entityModel -import dev.morphia.critter.parser.GeneratorTest.methodNames -import dev.morphia.mapping.codec.pojo.EntityModel -import dev.morphia.mapping.codec.pojo.PropertyModel -import dev.morphia.mapping.codec.pojo.critter.CritterPropertyModel -import java.lang.reflect.Method -import org.testng.Assert.assertEquals -import org.testng.annotations.DataProvider -import org.testng.annotations.NoInjection - -class TestPropertyModelGenerator : BaseCritterTest() { - val critterClassLoader = CritterClassLoader() - - // @Test(dataProvider = "properties", testName = "") - fun testProperty(control: String, methodName: String, @NoInjection method: Method) { - val propertyModel = getModel(control) - - println("exampleModel = [${control}], methodName = [${methodName}], method = [${method}]") - val expected = method.invoke(control) - val actual = method.invoke(propertyModel) - assertEquals(actual, expected, "${method.name} should return the same value") - } - - private fun getModel(name: String): CritterPropertyModel { - return entityModel.getProperty(name) as CritterPropertyModel - } - - @DataProvider(name = "properties") - fun methods(): Array> { - val methods = methodNames(CritterPropertyModel::class.java) - return listOf("dev.morphia.critter.sources.ExampleNamePropertyModelTemplate") - .map { loadModel(it) } - .flatMap { propertyModel -> - methods.map { method -> arrayOf(propertyModel.name, method[0], method[1]) } - } - .toTypedArray() - } - - private fun loadModel(type: String): PropertyModel { - return critterClassLoader - .loadClass(type) - .getConstructor(EntityModel::class.java) - .newInstance(entityModel) as PropertyModel - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestVarHandleAccessor.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestVarHandleAccessor.kt deleted file mode 100644 index e38a5decca3..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TestVarHandleAccessor.kt +++ /dev/null @@ -1,89 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.Critter.Companion.critterPackage -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator -import dev.morphia.critter.sources.Example -import dev.morphia.critter.titleCase -import org.bson.codecs.pojo.PropertyAccessor -import org.testng.Assert.assertEquals -import org.testng.Assert.assertNull -import org.testng.annotations.BeforeClass -import org.testng.annotations.Test - -class TestVarHandleAccessor { - private lateinit var classLoader: CritterClassLoader - - @BeforeClass - fun setup() { - classLoader = CritterClassLoader() - CritterGizmoGenerator.generate(Example::class.java, classLoader, runtimeMode = true) - } - - @Test - fun testEntityNotModified() { - // In runtime mode the entity class must NOT have synthetic __readXxx/__writeXxx methods - // (as opposed to the __readXxxTemplate methods that exist in the source) - val methods = Example::class.java.declaredMethods.map { it.name } - val syntheticRead = methods.filter { it.startsWith("__read") && !it.endsWith("Template") } - val syntheticWrite = methods.filter { it.startsWith("__write") && !it.endsWith("Template") } - assert(syntheticRead.isEmpty()) { - "Entity class should not have synthetic __read methods but found: $syntheticRead" - } - assert(syntheticWrite.isEmpty()) { - "Entity class should not have synthetic __write methods but found: $syntheticWrite" - } - } - - @Test - fun testStringField() { - val entity = Example() - val accessor = loadAccessor(Example::class.java, "name") - - assertNull(accessor.get(entity)) - accessor.set(entity, "hello") - assertEquals(accessor.get(entity), "hello") - } - - @Test - fun testIntPrimitiveField() { - val entity = Example() - val accessor = loadAccessor(Example::class.java, "age") - - // Default value is 21 (set in field initializer) - assertEquals(accessor.get(entity), 21) - accessor.set(entity, 42) - assertEquals(accessor.get(entity), 42) - } - - @Test - fun testLongBoxedField() { - val entity = Example() - val accessor = loadAccessor(Example::class.java, "salary") - - // Default value is 2L (set in field initializer) - assertEquals(accessor.get(entity), 2L) - accessor.set(entity, 100_000L) - assertEquals(accessor.get(entity), 100_000L) - } - - @Test - fun testAccessorsInstantiatable() { - // All three generated accessor classes must be loadable with no-arg constructor - listOf("name", "age", "salary").forEach { field -> - val cls = - classLoader.loadClass( - "${critterPackage(Example::class.java)}.${field.titleCase()}Accessor" - ) - cls.getConstructor().newInstance() - } - } - - @Suppress("UNCHECKED_CAST") - private fun loadAccessor(entityType: Class<*>, fieldName: String): PropertyAccessor { - val accessorClass = - classLoader.loadClass("${critterPackage(entityType)}.${fieldName.titleCase()}Accessor") - as Class> - return accessorClass.getConstructor().newInstance() - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TypesTest.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/TypesTest.kt deleted file mode 100644 index a70b18ead15..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/TypesTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.morphia.critter.parser - -import dev.morphia.critter.parser.Generators.asClass -import java.math.BigDecimal -import java.time.Instant -import java.util.Date -import java.util.Locale -import java.util.UUID -import org.objectweb.asm.Type -import org.testng.Assert.assertEquals -import org.testng.annotations.DataProvider -import org.testng.annotations.Test - -class TypesTest { - @DataProvider(name = "types") - fun typeProvider(): Array> { - return arrayOf( - // Primitive types - arrayOf(Boolean::class.java), - arrayOf(Char::class.java), - arrayOf(Byte::class.java), - arrayOf(Short::class.java), - arrayOf(Int::class.java), - arrayOf(Float::class.java), - arrayOf(Long::class.java), - arrayOf(Double::class.java), - - // Object types - arrayOf(String::class.java), - arrayOf(Locale::class.java), - arrayOf(Date::class.java), - arrayOf(UUID::class.java), - arrayOf(BigDecimal::class.java), - arrayOf(Instant::class.java), - - // Arrays of boxed primitives - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - - // Primitive arrays - arrayOf(BooleanArray::class.java), - arrayOf(CharArray::class.java), - arrayOf(ByteArray::class.java), - arrayOf(ShortArray::class.java), - arrayOf(IntArray::class.java), - arrayOf(FloatArray::class.java), - arrayOf(LongArray::class.java), - arrayOf(DoubleArray::class.java), - - // 2D primitive arrays - arrayOf(Array::class.java), - arrayOf(Array::class.java), - - // Arrays of objects - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - arrayOf(Array::class.java), - - // Arrays of arrays (2D) - arrayOf(Array>::class.java), - arrayOf(Array>::class.java), - arrayOf(Array>::class.java), - arrayOf(Array>::class.java), - - // Arrays of arrays of arrays (3D) - arrayOf(Array>>::class.java), - arrayOf(Array>>::class.java), - ) - } - - @Test(dataProvider = "types") - fun asClassConversion(expected: Class<*>) { - val type = Type.getType(expected) - val actual = type.asClass() - - assertEquals(actual, expected, "Type ${type.descriptor} should convert to ${expected.name}") - } -} diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt deleted file mode 100644 index 84d6a589b56..00000000000 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt +++ /dev/null @@ -1,402 +0,0 @@ -package dev.morphia.critter.parser.gizmo - -import com.mongodb.client.model.CollationCaseFirst.LOWER -import dev.morphia.annotations.Entity -import dev.morphia.annotations.EntityListeners -import dev.morphia.annotations.Indexes -import dev.morphia.annotations.internal.CollationBuilder.collationBuilder -import dev.morphia.annotations.internal.EntityBuilder.entityBuilder -import dev.morphia.annotations.internal.EntityListenersBuilder.entityListenersBuilder -import dev.morphia.annotations.internal.FieldBuilder.fieldBuilder -import dev.morphia.annotations.internal.IndexBuilder.indexBuilder -import dev.morphia.annotations.internal.IndexOptionsBuilder.indexOptionsBuilder -import dev.morphia.annotations.internal.IndexesBuilder.indexesBuilder -import dev.morphia.critter.ClassfileOutput -import dev.morphia.critter.CritterClassLoader -import dev.morphia.critter.parser.Generators.mapper -import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator as generator -import dev.morphia.critter.sources.Example -import dev.morphia.critter.sources.MethodExample -import dev.morphia.mapping.codec.pojo.EntityModel -import dev.morphia.mapping.codec.pojo.PropertyModel -import dev.morphia.mapping.codec.pojo.TypeData -import dev.morphia.mapping.lifecycle.EntityListenerAdapter -import io.quarkus.gizmo.ClassCreator -import io.quarkus.gizmo.MethodDescriptor -import io.quarkus.gizmo.MethodDescriptor.ofMethod -import java.lang.reflect.Modifier -import kotlin.reflect.KClass -import org.objectweb.asm.Type -import org.objectweb.asm.tree.AnnotationNode -import org.testng.Assert.assertEquals -import org.testng.Assert.assertFalse -import org.testng.Assert.assertNotNull -import org.testng.Assert.assertTrue -import org.testng.Assert.fail -import org.testng.annotations.Test - -class TestGizmoGeneration { - val critterClassLoader = CritterClassLoader() - - @Test - fun testMapStringExample() { - var descString = "Ljava/util/Map;" - var descriptor = - descriptor( - Map::class.java, - descriptor(String::class.java), - descriptor(Example::class.java), - ) - - assertEquals(descriptor, descString) - var typeData = typeData(descString)[0] - assertEquals( - typeData, - Map::class.typeData(String::class.typeData(), Example::class.typeData()), - ) - } - - @Test - fun testListMapStringExample() { - val descString = - "Ljava/util/List;>;" - val descriptor = - descriptor( - List::class.java, - descriptor( - Map::class.java, - descriptor(String::class.java), - descriptor(Example::class.java), - ), - ) - assertEquals(descriptor, descString) - - val typeData = typeData(descString)[0] - assertEquals( - typeData, - List::class.typeData( - Map::class.typeData(String::class.typeData(), Example::class.typeData()) - ), - ) - } - - @Test - fun testMapOfList() { - val descString = - "Ljava/util/Map;>;" - val descriptor = - descriptor( - Map::class.java, - descriptor(String::class.java), - descriptor(List::class.java, descriptor(Example::class.java)), - ) - assertEquals(descriptor, descString) - val typeData = typeData(descriptor)[0] - assertEquals( - typeData, - Map::class.typeData( - String::class.typeData(), - List::class.typeData(Example::class.typeData()), - ), - ) - } - - @Test - fun testPrimitiveArray() { - val typeData = typeData("[I")[0] - assertTrue(typeData.array) - } - - private fun descriptor(type: Class<*>, vararg typeParameters: String): String { - var desc = Type.getDescriptor(type) - if (typeParameters.isNotEmpty()) { - desc = - desc.dropLast(1) + - typeParameters.joinToString("", prefix = "<", postfix = ">") + - ";" - } - - return desc - } - - private fun KClass<*>.typeData(vararg typeParameters: TypeData<*>): TypeData<*> { - return TypeData(this.java, listOf(*typeParameters)) - } - - @Test - fun testAnnotationBuilding() { - val index = AnnotationNode("Ldev/morphia/annotations/Index;") - val field = AnnotationNode("Ldev/morphia/annotations/Field;") - // field.values = listOf("value", "name") - index.values = listOf("fields", listOf(field)) - ClassCreator.builder() - .className("critter.AnnotationTest") - .superClass(EntityModel::class.java) - .classOutput { name, data -> - val className = name.replace('/', '.') - critterClassLoader.register(className, data) - ClassfileOutput.dump(name, data) - } - .build() - .use { - val creator = it.getMethodCreator("test", Void::class.java) - val annotationMethod = - ofMethod( - EntityModel::class.java.name, - "annotation", - EntityModel::class.java.name, - Annotation::class.java, - ) - - creator.invokeVirtualMethod( - annotationMethod, - creator.`this`, - index.annotationBuilder(creator), - ) - } - } - - @Test - fun testGizmo() { - generator.generate(Example::class.java, critterClassLoader) - critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.AgeModel") - val nameModel = - critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.NameModel") - invokeAll(PropertyModel::class.java, nameModel) - critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.SalaryModel") - critterClassLoader - .loadClass("dev.morphia.critter.sources.__morphia.example.AgeAccessor") - .getConstructor() - .newInstance() - critterClassLoader - .loadClass("dev.morphia.critter.sources.__morphia.example.NameAccessor") - .getConstructor() - .newInstance() - critterClassLoader - .loadClass("dev.morphia.critter.sources.__morphia.example.SalaryAccessor") - .getConstructor() - .newInstance() - val loadClass = - critterClassLoader.loadClass( - "dev.morphia.critter.sources.__morphia.example.ExampleEntityModel" - ) - val constructors = loadClass.constructors - val model: EntityModel = constructors[0].newInstance(mapper) as EntityModel - validate(model) - } - - private fun validate(model: EntityModel) { - val annotation = model.getAnnotation(EntityListeners::class.java) - assertEquals( - annotation, - entityListenersBuilder().value(EntityListenerAdapter::class.java).build(), - ) - - assertEquals( - model.getAnnotation(Entity::class.java), - entityBuilder().value("examples").build(), - ) - - assertEquals( - model.getAnnotation(Indexes::class.java), - indexesBuilder() - .value( - indexBuilder() - .fields(fieldBuilder().value("name").weight(42).build()) - .options( - indexOptionsBuilder() - .partialFilter("partial filter") - .collation(collationBuilder().caseFirst(LOWER).build()) - .build() - ) - .build() - ) - .build(), - ) - - assertEquals(model.collectionName(), "examples") - assertEquals(model.discriminator(), "Example") - assertEquals(model.discriminatorKey(), "_t") - assertEquals(model.type.name, Example::class.java.name) - assertFalse(model.properties.isEmpty(), "Should have properties") - assertNotNull(model.idProperty, "Should have an ID property") - assertFalse(model.isAbstract(), "Should not be abstract") - assertFalse(model.isInterface(), "Should not be an interface") - assertTrue(model.useDiscriminator(), "Should use the discriminator") - assertTrue(model.classHierarchy().isEmpty(), "Should not have a class hierarchy") - } - - private fun invokeAll(type: Class<*>, klass: Class<*>) { - val instance = klass.constructors[0].newInstance(null) - val results = - type.declaredMethods - .filter { - Modifier.isPublic(it.modifiers) && - !Modifier.isFinal(it.modifiers) && - it.parameterCount == 0 - } - .filter { it.name !in listOf("hashCode", "toString") } - .sortedBy { it.name } - .map { method -> - try { - klass.getDeclaredMethod(method.name, *method.parameterTypes) - null - } catch (e: Exception) { - e.message - } - } - .filterNotNull() - - if (results.isNotEmpty()) { - fail("Missing methods from ${type.name}: \n${results.joinToString("\n")}") - } - } - - @Test - fun testConstructors() { - val className = "dev.morphia.critter.GizmoSubclass" - val constructorCall = - ClassCreator.builder() - .classOutput { name, data -> - critterClassLoader.register(name.replace('/', '.'), data) - } - .className("dev.morphia.critter.ConstructorCall") - .build() - val fieldCreator = - constructorCall - .getFieldCreator("name", String::class.java) - .setModifiers(Modifier.PUBLIC) - val constructorCreator = constructorCall.getConstructorCreator(String::class.java) - constructorCreator.invokeSpecialMethod( - MethodDescriptor.ofConstructor(Object::class.java), - constructorCreator.`this`, - ) - constructorCreator.setParameterNames(arrayOf("name")) - constructorCreator.writeInstanceField( - fieldCreator.fieldDescriptor, - constructorCreator.`this`, - constructorCreator.getMethodParam(0), - ) - - constructorCreator.returnVoid() - constructorCall.close() - val newInstance = - critterClassLoader - .loadClass("dev.morphia.critter.ConstructorCall") - .getConstructor(String::class.java) - .newInstance("here i am") - - val creator = - ClassCreator.builder() - .classOutput { name, data -> - critterClassLoader.register(name.replace('/', '.'), data) - } - .className(className) - .superClass("dev.morphia.critter.ConstructorCall") - .build() - val constructor = creator.getConstructorCreator(String::class.java) - constructor.invokeSpecialMethod( - MethodDescriptor.ofConstructor( - "dev.morphia.critter.ConstructorCall", - String::class.java, - ), - constructor.getThis(), - constructor.getMethodParam(0), - ) - constructor.setParameterNames(arrayOf("subName")) - constructor.returnVoid() - constructor.close() - creator.close() - val instance = - critterClassLoader - .loadClass(className) - .getConstructor(String::class.java) - .newInstance("This is my name") - - assertNotNull(instance) - } - - @Test - fun testMethodBasedAccessors() { - val classLoader = CritterClassLoader() - - // Convert to ASM MethodNodes - val resourceName = MethodExample::class.java.name.replace('.', '/') + ".class" - val inputStream = MethodExample::class.java.classLoader.getResourceAsStream(resourceName) - val classNode = org.objectweb.asm.tree.ClassNode() - org.objectweb.asm.ClassReader(inputStream).accept(classNode, 0) - - val methodNodes = - classNode.methods.filter { node -> - node.name.startsWith("get") && - Type.getArgumentTypes(node.desc).isEmpty() && - node.visibleAnnotations?.any { - it.desc in - listOf( - "Ldev/morphia/annotations/Id;", - "Ldev/morphia/annotations/Property;", - ) - } == true - } - - // Ensure we found the expected methods - val methodNames = methodNodes.map { it.name } - assertTrue(methodNames.contains("getId"), "Should find getId method") - assertTrue(methodNames.contains("getCount"), "Should find getCount method") - assertTrue(methodNames.contains("getScore"), "Should find getScore method") - assertTrue(methodNames.contains("getComputedValue"), "Should find getComputedValue method") - assertEquals(4, methodNodes.size, "Should find exactly 4 annotated getter methods") - - // Generate the accessor methods - val bytecode = - dev.morphia.critter.parser.asm - .AddMethodAccessorMethods(MethodExample::class.java, methodNodes) - .emit() - - // Register and load the modified class - classLoader.register(MethodExample::class.java.name, bytecode) - val modifiedClass = classLoader.loadClass(MethodExample::class.java.name) - - // Verify __read methods exist for all properties - assertNotNull(modifiedClass.getMethod("__readId"), "Should have __readId method") - assertNotNull(modifiedClass.getMethod("__readCount"), "Should have __readCount method") - assertNotNull(modifiedClass.getMethod("__readScore"), "Should have __readScore method") - assertNotNull( - modifiedClass.getMethod("__readComputedValue"), - "Should have __readComputedValue method", - ) - - // Verify __write methods exist for properties with setters - assertNotNull( - modifiedClass.getMethod("__writeId", org.bson.types.ObjectId::class.java), - "Should have __writeId method", - ) - assertNotNull( - modifiedClass.getMethod("__writeCount", Long::class.javaPrimitiveType), - "Should have __writeCount method", - ) - assertNotNull( - modifiedClass.getMethod("__writeScore", Double::class.javaPrimitiveType), - "Should have __writeScore method", - ) - - // Create an instance and test the read-only property throws exception - val instance = modifiedClass.getConstructor().newInstance() - val writeComputedMethod = - modifiedClass.getMethod("__writeComputedValue", String::class.java) - - try { - writeComputedMethod.invoke(instance, "test value") - fail("Should throw UnsupportedOperationException for read-only property") - } catch (e: java.lang.reflect.InvocationTargetException) { - assertTrue( - e.cause is UnsupportedOperationException, - "Should throw UnsupportedOperationException, got: ${e.cause}", - ) - assertTrue( - e.cause?.message?.contains("read-only") == true, - "Exception message should mention read-only", - ) - } - } -}