Skip to content

Commit a2ee008

Browse files
authored
Add JVM bytecode tests for ParparVM (#4221)
1 parent f9939db commit a2ee008

File tree

3 files changed

+391
-0
lines changed

3 files changed

+391
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: ParparVM Java Tests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'vm/**'
7+
- '!vm/**/README.md'
8+
- '!vm/**/readme.md'
9+
- '!vm/**/docs/**'
10+
push:
11+
branches: [ master, main ]
12+
paths:
13+
- 'vm/**'
14+
- '!vm/**/README.md'
15+
- '!vm/**/readme.md'
16+
- '!vm/**/docs/**'
17+
18+
jobs:
19+
vm-tests:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Check out repository
23+
uses: actions/checkout@v4
24+
25+
- name: Set up JDK 8
26+
uses: actions/setup-java@v4
27+
with:
28+
distribution: 'temurin'
29+
java-version: '8'
30+
cache: 'maven'
31+
32+
- name: Run ParparVM JVM tests
33+
working-directory: vm/tests
34+
run: mvn -B test

vm/tests/pom.xml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<groupId>com.codename1.parparvm</groupId>
7+
<artifactId>parparvm-tests</artifactId>
8+
<version>1.0-SNAPSHOT</version>
9+
<name>ParparVM Java Tests</name>
10+
11+
<properties>
12+
<maven.compiler.source>1.8</maven.compiler.source>
13+
<maven.compiler.target>1.8</maven.compiler.target>
14+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15+
<junit.jupiter.version>5.10.2</junit.jupiter.version>
16+
<asm.version>5.0.3</asm.version>
17+
</properties>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.ow2.asm</groupId>
22+
<artifactId>asm</artifactId>
23+
<version>${asm.version}</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.ow2.asm</groupId>
27+
<artifactId>asm-commons</artifactId>
28+
<version>${asm.version}</version>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.junit.jupiter</groupId>
32+
<artifactId>junit-jupiter</artifactId>
33+
<version>${junit.jupiter.version}</version>
34+
<scope>test</scope>
35+
</dependency>
36+
</dependencies>
37+
38+
<build>
39+
<plugins>
40+
<plugin>
41+
<groupId>org.apache.maven.plugins</groupId>
42+
<artifactId>maven-surefire-plugin</artifactId>
43+
<version>3.2.5</version>
44+
<configuration>
45+
<useModulePath>false</useModulePath>
46+
</configuration>
47+
</plugin>
48+
<plugin>
49+
<groupId>org.codehaus.mojo</groupId>
50+
<artifactId>build-helper-maven-plugin</artifactId>
51+
<version>3.5.0</version>
52+
<executions>
53+
<execution>
54+
<id>add-bytecodetranslator-sources</id>
55+
<phase>generate-sources</phase>
56+
<goals>
57+
<goal>add-source</goal>
58+
</goals>
59+
<configuration>
60+
<sources>
61+
<source>../ByteCodeTranslator/src</source>
62+
</sources>
63+
</configuration>
64+
</execution>
65+
</executions>
66+
</plugin>
67+
</plugins>
68+
</build>
69+
</project>
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package com.codename1.tools.translator;
2+
3+
import com.codename1.tools.translator.bytecodes.Instruction;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.objectweb.asm.ClassWriter;
7+
import org.objectweb.asm.MethodVisitor;
8+
import org.objectweb.asm.Opcodes;
9+
import org.objectweb.asm.Type;
10+
11+
import java.lang.reflect.Field;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
class ParserTest {
21+
22+
@BeforeEach
23+
void cleanParser() {
24+
Parser.cleanup();
25+
}
26+
27+
@Test
28+
void parsesBasicClassWithFieldsAndMethodBodies() throws Exception {
29+
Path classFile = createSampleClass();
30+
31+
Parser.parse(classFile.toFile());
32+
33+
ByteCodeClass cls = Parser.getClassObject("com_example_Sample");
34+
assertNotNull(cls, "Class should be parsed");
35+
assertEquals("java/lang/Object", cls.getBaseClass());
36+
assertFalse(cls.isIsInterface());
37+
assertFalse(cls.isIsAbstract());
38+
39+
ByteCodeField greeting = findField(cls, "GREETING");
40+
assertTrue(greeting.isStaticField());
41+
assertEquals("hi", greeting.getValue());
42+
43+
ByteCodeField names = findField(cls, "names");
44+
assertTrue(names.isObjectType(), "Generics-based list field should be an object type");
45+
assertTrue(names.getDependentClasses().contains("java_util_List"));
46+
47+
BytecodeMethod sum = findMethod(cls, "sum");
48+
assertFalse(sum.isStatic());
49+
List<Instruction> instructions = getInstructions(sum);
50+
List<Integer> opcodes = instructions.stream()
51+
.map(Instruction::getOpcode)
52+
.collect(Collectors.toList());
53+
assertTrue(opcodes.contains(Opcodes.IADD), "Method should include an integer add operation");
54+
assertEquals(Opcodes.IRETURN, opcodes.get(opcodes.size() - 1));
55+
}
56+
57+
@Test
58+
void parsesInterfacesAsAbstractContracts() throws Exception {
59+
Path classFile = createTaskInterface();
60+
61+
Parser.parse(classFile.toFile());
62+
63+
ByteCodeClass cls = Parser.getClassObject("com_example_Task");
64+
assertNotNull(cls);
65+
assertTrue(cls.isIsInterface());
66+
assertTrue(cls.isIsAbstract());
67+
68+
BytecodeMethod method = findMethod(cls, "runTask");
69+
assertTrue(method.canBeVirtual());
70+
}
71+
72+
@Test
73+
void parsesEnumMetadataAndBaseType() throws Exception {
74+
Path classFile = createPriorityEnum();
75+
76+
Parser.parse(classFile.toFile());
77+
78+
ByteCodeClass cls = Parser.getClassObject("com_example_Priority");
79+
assertNotNull(cls);
80+
assertEquals("java/lang/Enum", cls.getBaseClass());
81+
assertTrue(readPrivateBoolean(cls, "isEnum"));
82+
}
83+
84+
@Test
85+
void parsesAnnotationsWithCorrectFlags() throws Exception {
86+
Path classFile = createAnnotation();
87+
88+
Parser.parse(classFile.toFile());
89+
90+
ByteCodeClass cls = Parser.getClassObject("com_example_TestAnnotation");
91+
assertNotNull(cls);
92+
assertTrue(cls.isIsInterface(), "Annotations should be treated as interfaces");
93+
assertTrue(readPrivateBoolean(cls, "isAnnotation"));
94+
assertFalse(readPrivateBoolean(cls, "isSynthetic"));
95+
}
96+
97+
private ByteCodeField findField(ByteCodeClass cls, String name) {
98+
return cls.getFields()
99+
.stream()
100+
.filter(f -> f.getFieldName().equals(name))
101+
.findFirst()
102+
.orElseThrow(() -> new AssertionError("Field not found: " + name));
103+
}
104+
105+
private BytecodeMethod findMethod(ByteCodeClass cls, String name) {
106+
return cls.getMethods()
107+
.stream()
108+
.filter(m -> m.getMethodName().equals(name))
109+
.findFirst()
110+
.orElseThrow(() -> new AssertionError("Method not found: " + name));
111+
}
112+
113+
@SuppressWarnings("unchecked")
114+
private List<Instruction> getInstructions(BytecodeMethod method) throws Exception {
115+
Field instructionsField = BytecodeMethod.class.getDeclaredField("instructions");
116+
instructionsField.setAccessible(true);
117+
return (List<Instruction>) instructionsField.get(method);
118+
}
119+
120+
private boolean readPrivateBoolean(Object target, String fieldName) throws Exception {
121+
Field field = target.getClass().getDeclaredField(fieldName);
122+
field.setAccessible(true);
123+
return field.getBoolean(target);
124+
}
125+
126+
private Path createSampleClass() throws Exception {
127+
return writeClass("com/example/Sample", cw -> {
128+
cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "com/example/Sample", null, "java/lang/Object", null);
129+
cw.visitField(Opcodes.ACC_PRIVATE, "counter", "I", null, null).visitEnd();
130+
cw.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "GREETING", "Ljava/lang/String;", null, "hi").visitEnd();
131+
cw.visitField(Opcodes.ACC_PUBLIC, "names", "Ljava/util/List;", "Ljava/util/List<Ljava/lang/String;>;", null).visitEnd();
132+
133+
MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
134+
init.visitCode();
135+
init.visitVarInsn(Opcodes.ALOAD, 0);
136+
init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
137+
init.visitInsn(Opcodes.RETURN);
138+
init.visitMaxs(1, 1);
139+
init.visitEnd();
140+
141+
MethodVisitor sum = cw.visitMethod(Opcodes.ACC_PUBLIC, "sum", "(II)I", null, null);
142+
sum.visitCode();
143+
sum.visitVarInsn(Opcodes.ILOAD, 1);
144+
sum.visitVarInsn(Opcodes.ILOAD, 2);
145+
sum.visitInsn(Opcodes.IADD);
146+
sum.visitInsn(Opcodes.IRETURN);
147+
sum.visitMaxs(2, 3);
148+
sum.visitEnd();
149+
150+
cw.visitEnd();
151+
});
152+
}
153+
154+
private Path createTaskInterface() throws Exception {
155+
return writeClass("com/example/Task", cw -> {
156+
cw.visit(
157+
Opcodes.V1_5,
158+
Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE,
159+
"com/example/Task",
160+
null,
161+
"java/lang/Object",
162+
null
163+
);
164+
cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, "runTask", "()V", null, null).visitEnd();
165+
cw.visitEnd();
166+
});
167+
}
168+
169+
private Path createPriorityEnum() throws Exception {
170+
return writeClass("com/example/Priority", cw -> {
171+
cw.visit(
172+
Opcodes.V1_5,
173+
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER | Opcodes.ACC_ENUM,
174+
"com/example/Priority",
175+
null,
176+
"java/lang/Enum",
177+
null
178+
);
179+
cw.visitField(enumFieldFlags(), "LOW", "Lcom/example/Priority;", null, null).visitEnd();
180+
cw.visitField(enumFieldFlags(), "HIGH", "Lcom/example/Priority;", null, null).visitEnd();
181+
cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC, "$VALUES", "[Lcom/example/Priority;", null, null).visitEnd();
182+
183+
MethodVisitor ctor = cw.visitMethod(Opcodes.ACC_PRIVATE, "<init>", "(Ljava/lang/String;I)V", null, null);
184+
ctor.visitCode();
185+
ctor.visitVarInsn(Opcodes.ALOAD, 0);
186+
ctor.visitVarInsn(Opcodes.ALOAD, 1);
187+
ctor.visitVarInsn(Opcodes.ILOAD, 2);
188+
ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Enum", "<init>", "(Ljava/lang/String;I)V", false);
189+
ctor.visitInsn(Opcodes.RETURN);
190+
ctor.visitMaxs(3, 3);
191+
ctor.visitEnd();
192+
193+
MethodVisitor codeMethod = cw.visitMethod(Opcodes.ACC_PUBLIC, "code", "()I", null, null);
194+
codeMethod.visitCode();
195+
codeMethod.visitVarInsn(Opcodes.ALOAD, 0);
196+
codeMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Enum", "ordinal", "()I", false);
197+
codeMethod.visitInsn(Opcodes.IRETURN);
198+
codeMethod.visitMaxs(1, 1);
199+
codeMethod.visitEnd();
200+
201+
MethodVisitor values = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "values", "()[Lcom/example/Priority;", null, null);
202+
values.visitCode();
203+
values.visitFieldInsn(Opcodes.GETSTATIC, "com/example/Priority", "$VALUES", "[Lcom/example/Priority;");
204+
values.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "[Lcom/example/Priority;", "clone", "()Ljava/lang/Object;", false);
205+
values.visitTypeInsn(Opcodes.CHECKCAST, "[Lcom/example/Priority;");
206+
values.visitInsn(Opcodes.ARETURN);
207+
values.visitMaxs(1, 0);
208+
values.visitEnd();
209+
210+
MethodVisitor valueOf = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "valueOf", "(Ljava/lang/String;)Lcom/example/Priority;", null, null);
211+
valueOf.visitCode();
212+
valueOf.visitLdcInsn(Type.getType("Lcom/example/Priority;"));
213+
valueOf.visitVarInsn(Opcodes.ALOAD, 0);
214+
valueOf.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Enum", "valueOf", "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;", false);
215+
valueOf.visitTypeInsn(Opcodes.CHECKCAST, "com/example/Priority");
216+
valueOf.visitInsn(Opcodes.ARETURN);
217+
valueOf.visitMaxs(2, 1);
218+
valueOf.visitEnd();
219+
220+
MethodVisitor clinit = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
221+
clinit.visitCode();
222+
clinit.visitTypeInsn(Opcodes.NEW, "com/example/Priority");
223+
clinit.visitInsn(Opcodes.DUP);
224+
clinit.visitLdcInsn("LOW");
225+
clinit.visitInsn(Opcodes.ICONST_0);
226+
clinit.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/example/Priority", "<init>", "(Ljava/lang/String;I)V", false);
227+
clinit.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/Priority", "LOW", "Lcom/example/Priority;");
228+
229+
clinit.visitTypeInsn(Opcodes.NEW, "com/example/Priority");
230+
clinit.visitInsn(Opcodes.DUP);
231+
clinit.visitLdcInsn("HIGH");
232+
clinit.visitInsn(Opcodes.ICONST_1);
233+
clinit.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/example/Priority", "<init>", "(Ljava/lang/String;I)V", false);
234+
clinit.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/Priority", "HIGH", "Lcom/example/Priority;");
235+
236+
clinit.visitInsn(Opcodes.ICONST_2);
237+
clinit.visitTypeInsn(Opcodes.ANEWARRAY, "com/example/Priority");
238+
clinit.visitInsn(Opcodes.DUP);
239+
clinit.visitInsn(Opcodes.ICONST_0);
240+
clinit.visitFieldInsn(Opcodes.GETSTATIC, "com/example/Priority", "LOW", "Lcom/example/Priority;");
241+
clinit.visitInsn(Opcodes.AASTORE);
242+
clinit.visitInsn(Opcodes.DUP);
243+
clinit.visitInsn(Opcodes.ICONST_1);
244+
clinit.visitFieldInsn(Opcodes.GETSTATIC, "com/example/Priority", "HIGH", "Lcom/example/Priority;");
245+
clinit.visitInsn(Opcodes.AASTORE);
246+
clinit.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/Priority", "$VALUES", "[Lcom/example/Priority;");
247+
clinit.visitInsn(Opcodes.RETURN);
248+
clinit.visitMaxs(5, 0);
249+
clinit.visitEnd();
250+
251+
cw.visitEnd();
252+
});
253+
}
254+
255+
private Path createAnnotation() throws Exception {
256+
return writeClass("com/example/TestAnnotation", cw -> {
257+
cw.visit(
258+
Opcodes.V1_5,
259+
Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE | Opcodes.ACC_ANNOTATION,
260+
"com/example/TestAnnotation",
261+
null,
262+
"java/lang/Object",
263+
new String[]{"java/lang/annotation/Annotation"}
264+
);
265+
cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, "value", "()Ljava/lang/String;", null, null).visitEnd();
266+
cw.visitEnd();
267+
});
268+
}
269+
270+
private int enumFieldFlags() {
271+
return Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM;
272+
}
273+
274+
private Path writeClass(String internalName, ClassEmitter emitter) throws Exception {
275+
ClassWriter cw = new ClassWriter(0);
276+
emitter.accept(cw);
277+
Path outputDir = Files.createTempDirectory("parparvm-classes");
278+
Path classFile = outputDir.resolve(internalName + ".class");
279+
Files.createDirectories(classFile.getParent());
280+
Files.write(classFile, cw.toByteArray());
281+
return classFile;
282+
}
283+
284+
@FunctionalInterface
285+
private interface ClassEmitter {
286+
void accept(ClassWriter classWriter) throws Exception;
287+
}
288+
}

0 commit comments

Comments
 (0)