Commit f4858361 authored by Eric Bruneton's avatar Eric Bruneton
Browse files

Merge branch 'use-arrange-act-assert-pattern-in-tests' into 'master'

Use arrange act assert pattern in tests

See merge request !233
parents 340d1bed e9e27823
Pipeline #4162 passed with stage
in 11 minutes and 8 seconds
......@@ -27,6 +27,8 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.function.Consumer;
......@@ -44,6 +46,7 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
/**
* AdviceAdapter tests.
......@@ -406,8 +409,8 @@ public class AdviceAdapterTest extends AsmTest {
private static void testCase(final Consumer<MethodGenerator> testCaseGenerator) {
byte[] actualClass = generateClass(testCaseGenerator, /* expectedClass= */ false);
byte[] expectedClass = generateClass(testCaseGenerator, /* expectedClass= */ true);
assertThatClass(actualClass).isEqualTo(expectedClass);
loadAndInstantiate("C", actualClass);
assertEquals(new ClassFile(expectedClass), new ClassFile(actualClass));
assertDoesNotThrow(() -> new ClassFile(actualClass).newInstance());
}
private static byte[] generateClass(
......@@ -516,7 +519,9 @@ public class AdviceAdapterTest extends AsmTest {
}
classReader.accept(expectedClassVisitor, ClassReader.EXPAND_FRAMES);
classReader.accept(actualClassVisitor, ClassReader.EXPAND_FRAMES);
assertThatClass(actualClassWriter.toByteArray()).isEqualTo(expectedClassWriter.toByteArray());
assertEquals(
new ClassFile(expectedClassWriter.toByteArray()),
new ClassFile(actualClassWriter.toByteArray()));
}
private static class MethodGenerator extends MethodVisitor {
......
......@@ -27,8 +27,8 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.objectweb.asm.test.Assertions.assertThat;
import java.util.ArrayList;
import java.util.List;
......@@ -44,6 +44,7 @@ import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
/**
* AnalyzerAdapter tests.
......@@ -125,7 +126,7 @@ public class AnalyzerAdapterTest extends AsmTest {
Executable test =
() -> {
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
loadAndInstantiate(classParameter.getName(), classWriter.toByteArray());
new ClassFile(classWriter.toByteArray()).newInstance();
};
// jdk3.AllInstructions and jdk3.LargeMethod contain jsr/ret instructions,
// which are not supported.
......@@ -133,10 +134,10 @@ public class AnalyzerAdapterTest extends AsmTest {
|| classParameter == PrecompiledClass.JDK3_LARGE_METHOD
|| classParameter.isMoreRecentThan(apiParameter)) {
assertThrows(RuntimeException.class, test);
} else if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(UnsupportedClassVersionError.class, test);
} else {
assertThat(test)
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
assertDoesNotThrow(test);
}
}
......
......@@ -27,9 +27,9 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.objectweb.asm.test.Assertions.assertThat;
import java.util.Arrays;
import java.util.Locale;
......@@ -44,6 +44,7 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.util.CheckMethodAdapter;
......@@ -193,9 +194,12 @@ public class ClassRemapperTest extends AsmTest {
}
classReader.accept(classRemapper, 0);
byte[] classFile = classWriter.toByteArray();
assertThat(() -> loadAndInstantiate(upperCaseRemapper.getRemappedClassName(), classFile))
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(
UnsupportedClassVersionError.class, () -> new ClassFile(classFile).newInstance());
} else {
assertDoesNotThrow(() -> new ClassFile(classFile).newInstance());
}
}
/**
......@@ -219,9 +223,12 @@ public class ClassRemapperTest extends AsmTest {
}
classNode.accept(classRemapper);
byte[] classFile = classWriter.toByteArray();
assertThat(() -> loadAndInstantiate(upperCaseRemapper.getRemappedClassName(), classFile))
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(
UnsupportedClassVersionError.class, () -> new ClassFile(classFile).newInstance());
} else {
assertDoesNotThrow(() -> new ClassFile(classFile).newInstance());
}
}
private static void checkDescriptor(final String descriptor) {
......
......@@ -27,6 +27,7 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
......@@ -42,6 +43,7 @@ import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
/**
* CodeSizeEvaluator tests.
......@@ -112,7 +114,7 @@ public class CodeSizeEvaluatorTest extends AsmTest {
return;
}
classReader.accept(classVisitor, attributes(), 0);
assertThatClass(classWriter.toByteArray()).isEqualTo(classFile);
assertEquals(new ClassFile(classFile), new ClassFile(classWriter.toByteArray()));
}
private static Attribute[] attributes() {
......
......@@ -44,6 +44,7 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;
......@@ -233,7 +234,7 @@ public class InstructionAdapterTest extends AsmTest {
assertThrows(RuntimeException.class, () -> classReader.accept(classVisitor, attributes(), 0));
} else {
classReader.accept(classVisitor, attributes(), 0);
assertThatClass(classWriter.toByteArray()).isEqualTo(classFile);
assertEquals(new ClassFile(classFile), new ClassFile(classWriter.toByteArray()));
}
}
......
......@@ -27,10 +27,9 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.objectweb.asm.test.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
......@@ -43,6 +42,7 @@ import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;
......@@ -1464,9 +1464,13 @@ public class JsrInlinerAdapterTest extends AsmTest {
}
},
0);
assertThat(() -> loadAndInstantiate(classParameter.getName(), classWriter.toByteArray()))
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(
UnsupportedClassVersionError.class,
() -> new ClassFile(classWriter.toByteArray()).newInstance());
} else {
assertDoesNotThrow(() -> new ClassFile(classWriter.toByteArray()).newInstance());
}
}
private static class Generator {
......@@ -1610,7 +1614,7 @@ public class JsrInlinerAdapterTest extends AsmTest {
methodNode.accept(classWriter);
classWriter.visitEnd();
assertTrue(loadAndInstantiate("C", classWriter.toByteArray()));
assertDoesNotThrow(() -> new ClassFile(classWriter.toByteArray()).newInstance());
}
}
}
......@@ -27,9 +27,9 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.objectweb.asm.test.Assertions.assertThat;
import java.io.FileNotFoundException;
import java.io.IOException;
......@@ -45,6 +45,7 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
import org.objectweb.asm.tree.MethodNode;
/**
......@@ -130,9 +131,13 @@ public class LocalVariablesSorterTest extends AsmTest {
}
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
assertThat(() -> loadAndInstantiate(classParameter.getName(), classWriter.toByteArray()))
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(
UnsupportedClassVersionError.class,
() -> new ClassFile(classWriter.toByteArray()).newInstance());
} else {
assertDoesNotThrow(() -> new ClassFile(classWriter.toByteArray()).newInstance());
}
}
@Test
......@@ -157,6 +162,6 @@ public class LocalVariablesSorterTest extends AsmTest {
}
};
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
loadAndInstantiate("app1.Main$BadLocal", classWriter.toByteArray());
assertDoesNotThrow(() -> new ClassFile(classWriter.toByteArray()).newInstance());
}
}
......@@ -29,6 +29,7 @@ package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import org.junit.jupiter.api.Test;
......@@ -38,6 +39,7 @@ import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
/**
* SerialVersionUIDAdder tests.
......@@ -110,7 +112,7 @@ public class SerialVersionUidAdderTest extends AsmTest {
ClassWriter classWriter = new ClassWriter(0);
classReader.accept(new SerialVersionUIDAdder(classWriter), 0);
if ((classReader.getAccess() & Opcodes.ACC_ENUM) == 0) {
assertThatClass(classWriter.toByteArray()).contains("serialVersionUID");
assertTrue(new ClassFile(classWriter.toByteArray()).toString().contains("serialVersionUID"));
}
}
}
......@@ -27,8 +27,8 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.objectweb.asm.test.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
......@@ -39,6 +39,7 @@ import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.test.ClassFile;
/**
* TryCatchBlockSorter tests.
......@@ -81,8 +82,12 @@ public class TryCatchBlockSorterTest extends AsmTest {
};
classReader.accept(classVisitor, 0);
assertThat(() -> loadAndInstantiate(classParameter.getName(), classWriter.toByteArray()))
.succeedsOrThrows(UnsupportedClassVersionError.class)
.when(classParameter.isMoreRecentThanCurrentJdk());
if (classParameter.isMoreRecentThanCurrentJdk()) {
assertThrows(
UnsupportedClassVersionError.class,
() -> new ClassFile(classWriter.toByteArray()).newInstance());
} else {
assertDoesNotThrow(() -> new ClassFile(classWriter.toByteArray()).newInstance());
}
}
}
......@@ -27,21 +27,10 @@
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
......@@ -87,9 +76,6 @@ public abstract class AsmTest {
/** The size of the temporary byte array used to read class input streams chunk by chunk. */
private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096;
/** The name of JDK9 module classes. */
private static final String MODULE_INFO = "module-info";
/**
* MethodSource name to be used in parameterized tests that must be instantiated for all possible
* (precompiled class, api) pairs.
......@@ -153,7 +139,7 @@ public abstract class AsmTest {
* @return the internal name of this class.
*/
public String getInternalName() {
return name.endsWith(MODULE_INFO) ? MODULE_INFO : name.replace('.', '/');
return name.endsWith(ClassFile.MODULE_INFO) ? ClassFile.MODULE_INFO : name.replace('.', '/');
}
/**
......@@ -183,10 +169,10 @@ public abstract class AsmTest {
*/
public boolean isMoreRecentThanCurrentJdk() {
if (name.startsWith("jdk9.")) {
return getMajorJavaVersion() < 9;
return Util.getMajorJavaVersion() < 9;
}
if (name.startsWith("jdk11.")) {
return getMajorJavaVersion() < 11;
return Util.getMajorJavaVersion() < 11;
}
return false;
}
......@@ -311,91 +297,12 @@ public abstract class AsmTest {
Arrays.stream(apis).map(api -> Arguments.of(precompiledClass, api)));
}
private static int getMajorJavaVersion() {
String javaVersion = System.getProperty("java.version");
String javaMajorVersion = new StringTokenizer(javaVersion, "._").nextToken();
return Integer.parseInt(javaMajorVersion);
}
/**
* Starts an assertion about the given class content.
*
* @param classFile the content of a class.
* @return the {@link ClassSubject} to use to make actual assertions about the given class.
*/
public static ClassSubject assertThatClass(final byte[] classFile) {
return new ClassSubject(classFile);
}
/**
* Loads the given class in a new class loader. Also tries to instantiate the loaded class (if it
* is not an abstract or enum class, or a module-info class), in order to check that it passes the
* bytecode verification step. Checks as well that the class can be dumped, to make sure that the
* class is well formed.
*
* @param className the name of the class to load.
* @param classContent the content of the class to load.
* @return whether the class was loaded successfully.
*/
public static boolean loadAndInstantiate(final String className, final byte[] classContent) {
try {
new ClassDump(classContent);
} catch (IOException | IllegalArgumentException e) {
fail("Class can't be dumped, probably invalid", e);
}
if (className.endsWith(MODULE_INFO)) {
if (getMajorJavaVersion() < 9) {
throw new UnsupportedClassVersionError();
} else {
return true;
}
} else {
return doLoadAndInstantiate(className, classContent);
}
}
/**
* Loads the given class in a new class loader. Also tries to instantiate the loaded class (if it
* is not an abstract or enum class), in order to check that it passes the bytecode verification
* step.
*
* @param className the name of the class to load.
* @param classContent the content of the class to load.
* @return whether the class was loaded successfully.
*/
static boolean doLoadAndInstantiate(final String className, final byte[] classContent) {
ByteClassLoader byteClassLoader = new ByteClassLoader(className, classContent);
try {
Class<?> clazz = byteClassLoader.loadClass(className);
if (!clazz.isEnum() && (clazz.getModifiers() & Modifier.ABSTRACT) == 0) {
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
ArrayList<Object> arguments = new ArrayList<>();
for (Class<?> parameterType : constructor.getParameterTypes()) {
arguments.add(Array.get(Array.newInstance(parameterType, 1), 0));
}
constructor.setAccessible(true);
constructor.newInstance(arguments.toArray(new Object[0]));
}
} catch (ClassNotFoundException e) {
// Should never happen given the ByteClassLoader implementation.
fail("Can't find class " + className, e);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) {
// Should never happen since we don't try to instantiate classes that can't be instantiated
// (abstract and enum classes), we use setAccessible(true), and we create the appropriate
// constructor arguments for the expected constructor parameters.
fail("Can't instantiate class " + className, e);
} catch (InvocationTargetException e) {
// If an exception occurs in the invoked constructor, it means the class was successfully
// verified first. Still, we fail the test to be on the safe side.
fail("An exception occurred in the constructor of " + className, e);
}
return byteClassLoader.classLoaded();
}
private static byte[] getBytes(final String name) {
String resourceName = name.replace('.', '/') + ".class";
try (InputStream inputStream = ClassLoader.getSystemResourceAsStream(resourceName)) {
assertNotNull(inputStream, "Class not found " + name);
if (inputStream == null) {
throw new IllegalArgumentException("Class not found " + name);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = new byte[INPUT_STREAM_DATA_CHUNK_SIZE];
int bytesRead;
......@@ -405,77 +312,7 @@ public abstract class AsmTest {
outputStream.flush();
return outputStream.toByteArray();
} catch (IOException e) {
fail("Can't read " + name, e);
return new byte[0];
}
}
/** Helper to make assertions about a class. */
public static class ClassSubject {
/** The content of the class to be tested. */
private final byte[] classFile;
ClassSubject(final byte[] classFile) {
this.classFile = classFile;
}
/**
* Asserts that a dump of the subject class into a string representation contains the given
* string.
*
* @param expectedString a string which should be contained in a dump of the subject class.
*/
public void contains(final String expectedString) {
try {
String dump = new ClassDump(classFile).toString();
assertTrue(dump.contains(expectedString));
} catch (IOException | IllegalArgumentException e) {
fail("Class can't be dumped", e);
}
}
/**
* Asserts that the subject class is equal to the given class, modulo some low level bytecode
* representation details (e.g. the order of the constants in the constant pool, the order of
* attributes and annotations, and low level details such as ldc vs ldc_w instructions).
*
* @param expectedClassFile a class file content which should be equal to the subject class.
*/
public void isEqualTo(final byte[] expectedClassFile) {
try {
String dump = new ClassDump(classFile).toString();
String expectedDump = new ClassDump(expectedClassFile).toString();
assertEquals(expectedDump, dump);
} catch (IOException | IllegalArgumentException e) {
fail("Class can't be dumped", e);
}
}
}
/** A simple ClassLoader to test that a class can be loaded in the JVM. */
private static class ByteClassLoader extends ClassLoader {
private final String className;
private final byte[] classContent;
private boolean classLoaded;
ByteClassLoader(final String className, final byte[] classContent) {
this.className = className;
this.classContent = classContent;
}
boolean classLoaded() {
return classLoaded;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
if (name.equals(className)) {
classLoaded = true;
return defineClass(className, classContent, 0, classContent.length);
} else {
return super.loadClass(name, resolve);
}
throw new ClassFormatException("Can't read " + name, e);
}
}
}
......@@ -30,13 +30,17 @@ package org.objectweb.asm.test;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
/**
* A dump of the content of a class file, as a verbose, human "readable" String. As an example, the
* dump of the HelloWorld class is:
* A Java class file, whose content can be returned as a verbose, human "readable" string. As an
* example, the string representation of the HelloWorld class, obtained with the {@link #toString()}
* method, is:
*
* <pre>
* magic: -889275714
......@@ -112,46 +116,176 @@ import java.util.HashMap;
*
* @author Eric Bruneton
*/
class ClassDump {
public class ClassFile {
/** The dump of the input class. */
private final String dump;
/** The name of JDK9 module classes. */
static final String MODULE_INFO = "module-info";
/** The binary content of a Java class file. */