This library is a test kit to create fluent assertions for the ASM Java byte code modification framework, built on top of AssertJ.
⚠️ This library is still under development. Although the API is already very comprehensive, smaller parts are still missing (e.g., the support forModuleNode
s). And till the library does not reach its first major release, there may be minor API breaking changes.
ASM is a great framework to create and modify Java byte code. However, we face the challenge that errors in the byte code generation only become visible at runtime in the JVM. Therefore, good test coverage of the generated code is essential.
This library supports us in writing unit tests to prove that our modified byte code equals the one the Java compiler would generate from the source code.
Let's look at the capabilities of this test kit with an example. Suppose we want to generate the following simple method:
static void sayHello() {
System.out.println("Hello World");
}
The corresponding ASM logic would look like this:
private void generateSayHelloMethod(ClassNode classToModify) {
MethodNode sayHello = new MethodNode(Opcodes.ACC_STATIC, "sayHello", "()V", null, null);
var instructions = new InsnList();
instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
instructions.add(new LdcInsnNode("Hello World"));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));
instructions.add(new InsnNode(Opcodes.RETURN));
sayHello.instructions = instructions;
classToModify.methods.add(sayHello);
}
Next, we want to write a unit test for this logic.
For such a test, we first need a class file, which we can then modify using ASM. We can specify this as a String
directly in our test (line 3 in the following code). We then compile this Java source (line 8) and apply our ASM logic to the resulting class file (line 10). Finally, we reread the class file into the ClassNode
, containing our added method.
@Test
void testFieldGeneration() {
String actualSource = "class MyClass {}";
ClassNode actual = CompilationEnvironment
.create()
.addJavaInputSource(actualSource)
.compile();
// Reads class file and writes the modification back
.modifyClassNode("MyClass", this::generateSayHelloMethod)
// Reads modified class file
.readClassNode("MyClass");
...
}
In the next step, we want to compare the byte code we generated with the one the Java compiler would create. Therefore, we also define a String
that contains the Java source code of the method as if we were programming it in an IDE. Then we do the same as before, we compile this class and read the ClassNode
from the class file:
String expectedSource = "class MyClass {" +
" static void sayHello() { " +
" System.out.println(\"Hello World\"); " +
" }" +
"}";
ClassNode expected = CompilationEnvironment
.create()
.addJavaInputSource(expectedSource)
.compile()
.readClassNode("MyClass");
Finally, we have the actual ClassNode, which represents the byte code generated by our ASM logic, and the expected ClassNode, as the Java compiler would generate it. So we now only have to make sure that they are equal by using an ASM assertion from this library:
AsmAssertions.assertThat(actual)
.ignoreLineNumbers()
.isEqualTo(expected);
Since the output of the ASM assertions is based on the AssertJ output format, there is a welcome side effect: IDEs like IntelliJ offer us a link in the console output to open a diff window, which highlights the non-matching parts in the byte code.
Sometimes it is helpful to display the components of a class file in readable form for debugging. For this purpose, this library provides a set of AssertJ Representation classes that we can use to get a textualized form of an ASM node.
For example, we can get a String
representation of a class with the following call:
ClassNodeRepresentation.INSTANCE.toStringOf(actual)
And the output could look like this:
// Class version: 55
[32: super] class MyClass extends java.lang.Object
[0] <init>()
L0
LINENUMBER 1 L0
ALOAD 0 // opcode: 25
INVOKESPECIAL java/lang/Object.<init> ()V // opcode: 183
RETURN // opcode: 177
L1
// Local variable: #0 MyClass this // range: L0-L1
// Max locals: 1
// Max stack: 1
[8: static] void sayHello()
GETSTATIC java/lang/System.out : Ljava/io/PrintStream; // opcode: 178
LDC "Hello World" // opcode: 18
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V // opcode: 182
RETURN // opcode: 177
// Max locals: 0
// Max stack: 2
// Source file: MyClass.java
This library is available at Maven Central:
// Groovy
implementation 'dev.turingcomplete:asm-testkit:0.1.0'
// Kotlin
implementation("dev.turingcomplete:asm-testkit:0.1.0")
<dependency>
<groupId>dev.turingcomplete</groupId>
<artifactId>asm-testkit</artifactId>
<version>0.1.0</version>
</dependency>
The factory class AsmAssertions
is the main entry to create AssertJ assertions for ASM nodes:
assertThat(AccessNode actual)
assertThat(Attribute actual)
assertThat(AnnotationNode actual)
assertThat(TypeAnnotationNode actual)
assertThat(LocalVariableAnnotationNode actual)
assertThat(TypePath actual)
assertThat(Type actual)
assertThat(FieldNode actual)
assertThat(TypeReference actual)
assertThat(AbstractInsnNode actual)
assertThat(InsnList actual)
assertThat(LabelNode actual)
assertThat(LocalVariableNode actual)
assertThat(TryCatchBlockNode actual)
assertThat(ParameterNode actual)
assertThat(AnnotationDefaultNode actual)
assertThat(MethodNode actual)
assertThat(InnerClassNode actual)
assertThat(ClassNode actual)
An ASM node assert inherits from AbstractAssert
and has the same capabilities as typical AssertJ assertions, for example:
AsmAssertions.assertThat(classNodeA)
.isEqualTo(classNodeB);
Furthermore, there are additional factory methods to create assertions for an Iterable
of ASM nodes:
assertThatAttributes(Iterable<Attribute> actual)
assertThatAnnotations(Iterable<AnnotationNode> actual)
assertThatTypeAnnotations(Iterable<TypeAnnotationNode> actual)
assertThatLocalVariableAnnotations(Iterable<LocalVariableAnnotationNode> actual)
assertThatTypePaths(Iterable<TypePath> actual)
assertThatTypes(Iterable<Type> actual)
assertThatTypeReferences(Iterable<TypeReference> actual)
assertThatInstructions(Iterable<AbstractInsnNode> actual)
assertThatFields(Iterable<FieldNode> actual)
assertThatLabels(Iterable<LabelNode> actual)
assertThatLocalVariables(Iterable<LocalVariableNode> actual)
assertThatTryCatchBlocks(Iterable<TryCatchBlockNode> actual)
assertThatParameters(Iterable<ParameterNode> actual)
assertThatAnnotationDefaulls(Iterable<AnnotationDefaultNode> actual)
assertThatAccesses(Iterable<AccessNode> actual)
assertThatMethods(Iterable<MethodNode> actual)
assertThatInnerClasses(Iterable<InnerClassNode> actual)
assertThatClasses(Iterable<ClassNode> actual)
These assertions inherit from AbstractIterableAssert and have a wide range of capabilities to check the characteristics of a collection.
However, you should note that these assertions are preferable to be used with containsExactlyInAnyOrderElementsOf
or containsExactlyInAnyOrderCompareOneByOneElementsOf
. An exception to this is assertThatInstructions, here isEqual should be used (because the order is essential). All other AssertJ assertions methods should work but may not utilize the full functionality of the ASM test kit. If you need them, feel free to create an issue.
Analogous to the assertions, there are java.util.Comparator
s for ASM nodes, which we can use to define an order, or at least use it to check the equality of two nodes. They are in the package dev.turingcomplete.asmtestkit.comparator
and are mainly used as a backbone for the ASM related AbstractIterableAssert
s.
All ASM comparators have at least two constant fields INSTANCE
and INSTANCE_ITERABLE
, which provide a reusable instance for a single ASM node or a Iterable
of nodes.
The mother of all comparators is the DefaultAsmComparator
s. This class bundles multiple comparator instances and can return a specific instance for a given ASM class:
Comparator<MethodNode> methodNodeComparator = DefaultAsmComparators.INSTANCE.elementComparator(MethodNode.clsss);
The purpose behind this overclass is the hierarchy of operators. For example, a comparator for ClassNode
s uses the comparator for MethodNode
s, which uses the comparator for InsnList
.
Readable textual representation of ASM nodes can be created using the AssertJ org.assertj.core.presentation.Representation
s from the package dev.turingcomplete.asmtestkit.representation
.
The primary representation method is #toStringOf(Object)
, which creates a complete representation of an ASM node. In addition, some support the #toSimplifiedStringOf(Object)
functionality, which we can use to produce a short, single-line output. For example, for a MethodNode
, only the method header but not the body will be output.
All ASM representations have a constant field INSTANCE
with a reusable instance.
There is also an overclass with DefaultAsmRepresentations
that bundles all representations, which functions similarly to the one of for the comparators:
AsmRepresentation<MethodNode> methodNodeRepresentation = DefaultAsmRepresentations.INSTANCE.getAsmRepresentation(MethodNode.class);
This class is also a Representation
and can therefore be used as a single entry for any ASM node:
DefaultAsmRepresentations.INSTANCE.toStringOf(methodNode);
DefaultAsmRepresentations.INSTANCE.toStringOf(fieldNode);
...
The assertions and comparators to check ClassNodes
s, MethodNodess and InsnList
s can ignore LineNumberNode
s and their associated LabelNode
s, by calling the method ignoreLineNumbers()
.
- Implement assertions, comparators and representations for:
- ModuleNode
- ModuleRequireNode
- ModuleExportNode
- ModuleOpenNode
- ModuleProvideNode
- RecordNode
- Some of the
AsmAssert
s don't make use ofStandardAssertOption.IGNORE_*
yet - Add
AssertOption
s to ignore someFieldNode
s andMethodNode
s.
Copyright (c) 2022 Marcel Kliemannel
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the LICENSE for the specific language governing permissions and limitations under the License.