Skip to content

Commit 0ce4cb9

Browse files
evanchoolyclaude
andauthored
Phase 3.1: Convert critter-core test sources from Kotlin to Java (#4196)
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 <noreply@anthropic.com>
1 parent 1f3a3bf commit 0ce4cb9

18 files changed

+890
-997
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package dev.morphia.critter;
2+
3+
import java.io.InputStream;
4+
import java.io.PrintWriter;
5+
import java.io.StringWriter;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.Comparator;
9+
10+
import org.jboss.windup.decompiler.fernflower.FernflowerDecompiler;
11+
import org.objectweb.asm.ClassReader;
12+
import org.objectweb.asm.util.ASMifier;
13+
import org.objectweb.asm.util.TraceClassVisitor;
14+
15+
public class ClassfileOutput {
16+
17+
public static void dump(CritterClassLoader classLoader, String className) throws Exception {
18+
dump(classLoader, className, Path.of("target/dumps/"));
19+
}
20+
21+
public static void dump(CritterClassLoader classLoader, String className, Path outputDir) throws Exception {
22+
byte[] bytes = classLoader.getTypeDefinitions().get(className);
23+
if (bytes == null) {
24+
String resourceName = className.replace('.', '/') + ".class";
25+
InputStream stream = classLoader.getResourceAsStream(resourceName);
26+
if (stream == null)
27+
return;
28+
bytes = stream.readAllBytes();
29+
}
30+
String[][] outputs = {
31+
{ "javap", dumpBytecode(bytes) },
32+
{ "asm", dumpAsmSource(bytes) },
33+
{ "java", decompile(bytes) }
34+
};
35+
for (String[] entry : outputs) {
36+
Path output = outputDir.resolve(className.replace('.', '/') + "." + entry[0]);
37+
output.toFile().getParentFile().mkdirs();
38+
Files.writeString(output, entry[1]);
39+
}
40+
}
41+
42+
public static void dump(String className, byte[] bytes) throws Exception {
43+
dump(className, bytes, Path.of("target/dumps/"));
44+
}
45+
46+
public static void dump(String className, byte[] bytes, Path outputDir) throws Exception {
47+
String[][] outputs = {
48+
{ "javap", dumpBytecode(bytes) },
49+
{ "asm", dumpAsmSource(bytes) },
50+
{ "java", decompile(bytes) }
51+
};
52+
for (String[] entry : outputs) {
53+
Path output = outputDir.resolve(className + "." + entry[0]);
54+
output.toFile().getParentFile().mkdirs();
55+
Files.writeString(output, entry[1]);
56+
}
57+
}
58+
59+
public static String dumpBytecode(Class<?> clazz) throws Exception {
60+
ClassReader classReader = new ClassReader(clazz.getName());
61+
StringWriter sw = new StringWriter();
62+
classReader.accept(new TraceClassVisitor(new PrintWriter(sw)), 0);
63+
return sw.toString();
64+
}
65+
66+
public static String dumpBytecode(byte[] bytecode) {
67+
ClassReader classReader = new ClassReader(bytecode);
68+
StringWriter sw = new StringWriter();
69+
classReader.accept(new TraceClassVisitor(new PrintWriter(sw)), 0);
70+
return sw.toString();
71+
}
72+
73+
public static String dumpAsmSource(Class<?> clazz) throws Exception {
74+
ClassReader classReader = new ClassReader(clazz.getName());
75+
StringWriter sw = new StringWriter();
76+
classReader.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(sw)), 0);
77+
return sw.toString();
78+
}
79+
80+
public static String dumpAsmSource(byte[] bytecode) {
81+
ClassReader classReader = new ClassReader(bytecode);
82+
StringWriter sw = new StringWriter();
83+
classReader.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(sw)), 0);
84+
return sw.toString();
85+
}
86+
87+
public static String decompile(Class<?> clazz) throws Exception {
88+
String resourceName = clazz.getName().replace('.', '/') + ".class";
89+
InputStream stream = clazz.getClassLoader().getResourceAsStream(resourceName);
90+
if (stream == null) {
91+
throw new IllegalArgumentException("Cannot find class file for " + clazz.getName());
92+
}
93+
return decompile(stream.readAllBytes());
94+
}
95+
96+
public static String decompile(byte[] bytecode) throws Exception {
97+
Path tempDir = Files.createTempDirectory("fernflower");
98+
Path outputDir = Files.createTempDirectory("fernflower-output");
99+
try {
100+
Path classFile = tempDir.resolve("TempClass.class");
101+
Files.write(classFile, bytecode);
102+
103+
FernflowerDecompiler decompiler = new FernflowerDecompiler();
104+
decompiler.decompileClassFile(tempDir, classFile, outputDir);
105+
106+
Path decompiledFile = outputDir.resolve("TempClass.java");
107+
if (Files.exists(decompiledFile)) {
108+
return Files.readString(decompiledFile);
109+
}
110+
return Files.walk(outputDir)
111+
.filter(p -> p.toString().endsWith(".java"))
112+
.findFirst()
113+
.map(p -> {
114+
try {
115+
return Files.readString(p);
116+
} catch (Exception e) {
117+
return "// Decompilation failed";
118+
}
119+
})
120+
.orElse("// Decompilation failed");
121+
} finally {
122+
deleteRecursively(tempDir);
123+
deleteRecursively(outputDir);
124+
}
125+
}
126+
127+
private static void deleteRecursively(Path dir) {
128+
if (!Files.exists(dir))
129+
return;
130+
try {
131+
Files.walk(dir)
132+
.sorted(Comparator.reverseOrder())
133+
.forEach(p -> {
134+
try {
135+
Files.delete(p);
136+
} catch (Exception ignored) {
137+
}
138+
});
139+
} catch (Exception ignored) {
140+
}
141+
}
142+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.morphia.critter.parser;
2+
3+
import dev.morphia.mapping.codec.pojo.EntityModel;
4+
5+
public class BaseCritterTest {
6+
protected EntityModel exampleEntityModel = new EntityModel(String.class);
7+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dev.morphia.critter.parser;
2+
3+
import java.lang.reflect.Method;
4+
import java.lang.reflect.Modifier;
5+
import java.util.ArrayList;
6+
import java.util.Arrays;
7+
import java.util.Comparator;
8+
import java.util.List;
9+
import java.util.function.Predicate;
10+
import java.util.stream.Collectors;
11+
12+
import dev.morphia.critter.ClassfileOutput;
13+
import dev.morphia.critter.CritterClassLoader;
14+
import dev.morphia.critter.parser.gizmo.CritterGizmoGenerator;
15+
import dev.morphia.critter.parser.gizmo.GizmoEntityModelGenerator;
16+
import dev.morphia.critter.sources.Example;
17+
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;
18+
19+
import io.github.classgraph.ClassGraph;
20+
21+
public class GeneratorTest {
22+
public static final CritterEntityModel entityModel;
23+
public static final CritterClassLoader critterClassLoader = new CritterClassLoader();
24+
25+
static {
26+
ClassGraph classGraph = new ClassGraph()
27+
.addClassLoader(critterClassLoader)
28+
.enableAllInfo();
29+
classGraph.acceptPackages("dev.morphia.critter.sources");
30+
31+
try (var scanResult = classGraph.scan()) {
32+
for (var classInfo : scanResult.getAllClasses()) {
33+
try {
34+
ClassfileOutput.dump(critterClassLoader, classInfo.getName());
35+
} catch (Throwable ignored) {
36+
}
37+
}
38+
} catch (Exception ignored) {
39+
}
40+
41+
GizmoEntityModelGenerator gen = CritterGizmoGenerator.INSTANCE.generate(Example.class, critterClassLoader, false);
42+
try {
43+
entityModel = (CritterEntityModel) critterClassLoader
44+
.loadClass(gen.getGeneratedType())
45+
.getConstructors()[0]
46+
.newInstance(Generators.INSTANCE.getMapper());
47+
} catch (Exception e) {
48+
throw new RuntimeException(e);
49+
}
50+
}
51+
52+
public static Object[][] methodNames(Class<?> clazz) {
53+
return methods(clazz).stream()
54+
.map(m -> new Object[] { m.getName(), m })
55+
.sorted(Comparator.comparing(a -> a[0].toString()))
56+
.toArray(Object[][]::new);
57+
}
58+
59+
public static List<Method> methods(Class<?> clazz) {
60+
return Arrays.stream(clazz.getMethods())
61+
.filter(m -> !Modifier.isFinal(m.getModifiers()))
62+
.filter(m -> m.getParameterCount() == 0)
63+
.filter(m -> m.getDeclaringClass() == clazz)
64+
.collect(Collectors.toList());
65+
}
66+
67+
/** Helper: remove list elements while predicate holds, then remove one more, return joined string. */
68+
static String removeWhile(List<String> list, Predicate<String> predicate) {
69+
List<String> removed = new ArrayList<>();
70+
while (!list.isEmpty() && predicate.test(list.get(0))) {
71+
removed.add(list.remove(0));
72+
}
73+
if (!list.isEmpty()) {
74+
removed.add(list.remove(0));
75+
}
76+
return String.join("\n", removed);
77+
}
78+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package dev.morphia.critter.parser;
2+
3+
import java.util.List;
4+
5+
import dev.morphia.critter.Critter;
6+
import dev.morphia.critter.CritterClassLoader;
7+
import dev.morphia.critter.CritterKt;
8+
import dev.morphia.critter.sources.Example;
9+
10+
import org.bson.codecs.pojo.PropertyAccessor;
11+
import org.testng.Assert;
12+
import org.testng.annotations.DataProvider;
13+
14+
public class TestAccessorsMutators extends BaseCritterTest {
15+
private final CritterClassLoader critterClassLoader = new CritterClassLoader();
16+
17+
// @Test(dataProvider = "classes")
18+
public void testPropertyAccessors(Class<?> type) throws Exception {
19+
List<List<Object>> testFields = List.of(
20+
List.of("name", String.class, "set externally"),
21+
List.of("age", int.class, 100),
22+
List.of("salary", Long.class, 100_000L));
23+
24+
Object entity = critterClassLoader.loadClass(type.getName()).getConstructor().newInstance();
25+
26+
for (List<Object> field : testFields) {
27+
testAccessor(type, critterClassLoader, entity, (String) field.get(0), field.get(2));
28+
}
29+
}
30+
31+
@SuppressWarnings("unchecked")
32+
private void testAccessor(
33+
Class<?> type,
34+
CritterClassLoader loader,
35+
Object entity,
36+
String fieldName,
37+
Object testValue) throws Exception {
38+
Class<PropertyAccessor<Object>> accessorClass = (Class<PropertyAccessor<Object>>) loader.loadClass(
39+
Critter.Companion.critterPackage(type)
40+
+ type.getSimpleName()
41+
+ CritterKt.titleCase(fieldName)
42+
+ "Accessor");
43+
PropertyAccessor<Object> accessor = accessorClass.getConstructor().newInstance();
44+
45+
accessor.set(entity, testValue);
46+
Assert.assertEquals(accessor.get(entity), testValue);
47+
Assert.assertTrue(
48+
entity.toString().contains(testValue.toString()),
49+
"Could not find '" + testValue + "` in :" + entity);
50+
}
51+
52+
@DataProvider(name = "classes")
53+
public Object[][] names() {
54+
return new Object[][] { { Example.class } };
55+
}
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package dev.morphia.critter.parser;
2+
3+
import java.lang.reflect.Method;
4+
5+
import dev.morphia.critter.ClassfileOutput;
6+
import dev.morphia.critter.CritterClassLoader;
7+
import dev.morphia.mapping.Mapper;
8+
import dev.morphia.mapping.ReflectiveMapper;
9+
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;
10+
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
import org.testng.Assert;
14+
import org.testng.annotations.DataProvider;
15+
import org.testng.annotations.NoInjection;
16+
17+
public class TestEntityModelGenerator {
18+
private static final Logger LOG = LoggerFactory.getLogger(TestEntityModelGenerator.class);
19+
20+
private final CritterEntityModel control;
21+
private final Mapper mapper = new ReflectiveMapper(Generators.INSTANCE.getConfig());
22+
private final CritterClassLoader critterClassLoader = new CritterClassLoader();
23+
24+
public TestEntityModelGenerator() {
25+
CritterEntityModel tmp;
26+
try {
27+
tmp = (CritterEntityModel) critterClassLoader
28+
.loadClass("dev.morphia.critter.sources.ExampleEntityModelTemplate")
29+
.getConstructor(Mapper.class)
30+
.newInstance(mapper);
31+
ClassfileOutput.dump(critterClassLoader, "dev.morphia.critter.sources.ExampleEntityModelTemplate");
32+
} catch (Exception e) {
33+
LOG.error(e.getMessage(), e);
34+
throw new RuntimeException(e);
35+
}
36+
control = tmp;
37+
}
38+
39+
// @Test(dataProvider = "methods")
40+
public void testEntityModel(String name, @NoInjection Method method) throws Exception {
41+
Object expected = method.invoke(control);
42+
Object actual = method.invoke(GeneratorTest.entityModel);
43+
Assert.assertEquals(actual, expected, method.getName() + " should return the same value");
44+
}
45+
46+
@DataProvider(name = "methods")
47+
public Object[][] methods() {
48+
return GeneratorTest.methodNames(CritterEntityModel.class);
49+
}
50+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dev.morphia.critter.parser;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
7+
import dev.morphia.critter.CritterClassLoader;
8+
import dev.morphia.mapping.codec.pojo.EntityModel;
9+
import dev.morphia.mapping.codec.pojo.PropertyModel;
10+
import dev.morphia.mapping.codec.pojo.critter.CritterPropertyModel;
11+
12+
import org.testng.Assert;
13+
import org.testng.annotations.DataProvider;
14+
import org.testng.annotations.NoInjection;
15+
16+
public class TestPropertyModelGenerator extends BaseCritterTest {
17+
private final CritterClassLoader critterClassLoader = new CritterClassLoader();
18+
19+
// @Test(dataProvider = "properties", testName = "")
20+
public void testProperty(String control, String methodName, @NoInjection Method method) throws Exception {
21+
CritterPropertyModel propertyModel = getModel(control);
22+
System.out.println("exampleModel = [" + control + "], methodName = [" + methodName + "], method = [" + method + "]");
23+
Object expected = method.invoke(control);
24+
Object actual = method.invoke(propertyModel);
25+
Assert.assertEquals(actual, expected, method.getName() + " should return the same value");
26+
}
27+
28+
private CritterPropertyModel getModel(String name) {
29+
return (CritterPropertyModel) GeneratorTest.entityModel.getProperty(name);
30+
}
31+
32+
@DataProvider(name = "properties")
33+
public Object[][] methods() {
34+
Object[][] methods = GeneratorTest.methodNames(CritterPropertyModel.class);
35+
return List.of("dev.morphia.critter.sources.ExampleNamePropertyModelTemplate").stream()
36+
.map(type -> {
37+
try {
38+
return loadModel(type);
39+
} catch (Exception e) {
40+
throw new RuntimeException(e);
41+
}
42+
})
43+
.flatMap(propertyModel -> Arrays.stream(methods)
44+
.map(method -> new Object[] { propertyModel.getName(), method[0], method[1] }))
45+
.toArray(Object[][]::new);
46+
}
47+
48+
private PropertyModel loadModel(String type) throws Exception {
49+
return (PropertyModel) critterClassLoader
50+
.loadClass(type)
51+
.getConstructor(EntityModel.class)
52+
.newInstance(GeneratorTest.entityModel);
53+
}
54+
}

0 commit comments

Comments
 (0)