JUnit: add support for TestNG annotations

This commit is contained in:
Alexey Andreev 2021-03-16 20:18:58 +03:00
parent e5c3d144e8
commit 71f87d79a5
11 changed files with 593 additions and 173 deletions

View File

@ -140,5 +140,10 @@ class DependencyClassSource implements ClassHolderSource {
public boolean isStrict() {
return strict;
}
@Override
public void submit(ClassHolder cls) {
DependencyClassSource.this.submit(cls);
}
};
}

View File

@ -58,13 +58,7 @@ class VirtualCallConsumer implements DependencyConsumer {
knownTypes.set(type.index);
String className = type.getName();
/*
if (DependencyAnalyzer.shouldLog) {
System.out.println("Virtual call of " + methodDesc + " detected on " + node.getTag() + ". "
+ "Target class is " + className);
}
*/
if (className.startsWith("[")) {
className = "java.lang.Object";
}

View File

@ -28,4 +28,6 @@ public interface ClassHolderTransformerContext {
boolean isObfuscated();
boolean isStrict();
void submit(ClassHolder cls);
}

View File

@ -75,6 +75,7 @@
<java-tests.version>11</java-tests.version>
<rhino.version>1.7.11</rhino.version>
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
<testng.version>7.1.0</testng.version>
<junit.version>4.13.2</junit.version>
<commons-cli.version>1.4</commons-cli.version>
@ -212,6 +213,11 @@
<artifactId>rhino</artifactId>
<version>${rhino.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>

View File

@ -35,6 +35,11 @@
<artifactId>junit</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-tooling</artifactId>

View File

@ -28,6 +28,7 @@ import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -49,10 +50,6 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import junit.framework.TestCase;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
@ -74,11 +71,13 @@ import org.teavm.dependency.PreciseDependencyAnalyzer;
import org.teavm.diagnostics.DefaultProblemTextConsumer;
import org.teavm.diagnostics.Problem;
import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader;
import org.teavm.model.AnnotationValue;
import org.teavm.model.ClassHolder;
import org.teavm.model.ClassHolderSource;
import org.teavm.model.MethodDescriptor;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.model.PreOptimizingClassHolderSource;
import org.teavm.model.ReferenceCache;
@ -96,8 +95,14 @@ public class TeaVMTestRunner extends Runner implements Filterable {
static final MethodReference JUNIT3_BEFORE = new MethodReference(JUNIT3_BASE_CLASS, "setUp", ValueType.VOID);
static final MethodReference JUNIT3_AFTER = new MethodReference(JUNIT3_BASE_CLASS, "tearDown", ValueType.VOID);
static final String JUNIT4_TEST = "org.junit.Test";
static final String JUNIT4_IGNORE = "org.junit.Ignore";
static final String TESTNG_TEST = "org.testng.annotations.Test";
static final String TESTNG_IGNORE = "org.testng.annotations.Ignore";
static final String JUNIT4_BEFORE = "org.junit.Before";
static final String TESTNG_BEFORE = "org.testng.annotations.BeforeMethod";
static final String JUNIT4_AFTER = "org.junit.After";
static final String TESTNG_AFTER = "org.testng.annotations.AfterMethod";
static final String TESTNG_PROVIDER = "org.testng.annotations.DataProvider";
private static final String PATH_PARAM = "teavm.junit.target";
private static final String JS_RUNNER = "teavm.junit.js.runner";
private static final String THREAD_COUNT = "teavm.junit.threads";
@ -335,11 +340,17 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
private boolean isTestMethod(Method method) {
if (!Modifier.isPublic(method.getModifiers())) {
return false;
}
if (TestCase.class.isAssignableFrom(method.getDeclaringClass())) {
return method.getName().startsWith("test") && method.getName().length() > 4
&& Character.isUpperCase(method.getName().charAt(4));
} else if (getClassAnnotation(method, TESTNG_TEST) != null) {
return method.getName().startsWith("test_");
} else {
return method.isAnnotationPresent(Test.class);
return getAnnotation(method, JUNIT4_TEST) != null || getAnnotation(method, TESTNG_TEST) != null;
}
}
@ -391,7 +402,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
Description description = describeChild(child);
notifier.fireTestStarted(description);
if (child.isAnnotationPresent(Ignore.class)) {
if (isIgnored(child)) {
notifier.fireTestIgnored(description);
latch.countDown();
return;
@ -400,23 +411,11 @@ public class TeaVMTestRunner extends Runner implements Filterable {
boolean ran = false;
boolean success = true;
ClassHolder classHolder = classSource.get(child.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(child));
Set<Class<?>> expectedExceptions = new HashSet<>();
for (String exceptionName : getExpectedExceptions(methodHolder)) {
try {
expectedExceptions.add(Class.forName(exceptionName, false, classLoader));
} catch (ClassNotFoundException e) {
notifier.fireTestFailure(new Failure(description, e));
notifier.fireTestFinished(description);
latch.countDown();
return;
}
}
if (!child.isAnnotationPresent(SkipJVM.class) && !testClass.isAnnotationPresent(SkipJVM.class)) {
ran = true;
success = runInJvm(child, notifier, expectedExceptions);
ClassHolder classHolder = classSource.get(child.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(child));
success = runInJvm(child, notifier, getExpectedExceptions(methodHolder));
}
if (success && outputDir != null) {
@ -459,7 +458,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
File outputPath = getOutputPathForClass();
File outputPathForMethod = getOutputPath(child);
MethodDescriptor descriptor = getDescriptor(child);
MethodReference reference = new MethodReference(testClass.getName(), descriptor);
MethodReference reference = new MethodReference(child.getDeclaringClass().getName(), descriptor);
File testFilePath = getOutputPath(child);
testFilePath.mkdirs();
@ -467,7 +466,8 @@ public class TeaVMTestRunner extends Runner implements Filterable {
Map<String, String> properties = new HashMap<>();
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), false, ".js");
runs.add(createTestRun(testPath, child, RunKind.JAVASCRIPT, reference.toString(), notifier, onSuccess));
runs.add(createTestRun(configuration, testPath, child, RunKind.JAVASCRIPT, reference.toString(),
notifier, onSuccess));
File htmlPath = getOutputFile(outputPathForMethod, "test", configuration.getSuffix(), false, ".html");
properties.put("SCRIPT", "../" + testPath.getName());
properties.put("IDENTIFIER", reference.toString());
@ -480,12 +480,14 @@ public class TeaVMTestRunner extends Runner implements Filterable {
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), false, ".wasm");
runs.add(createTestRun(testPath, child, RunKind.WASM, reference.toString(), notifier, onSuccess));
runs.add(createTestRun(configuration, testPath, child, RunKind.WASM, reference.toString(),
notifier, onSuccess));
}
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), true, ".c");
runs.add(createTestRun(testPath, child, RunKind.C, reference.toString(), notifier, onSuccess));
runs.add(createTestRun(configuration, testPath, child, RunKind.C, reference.toString(),
notifier, onSuccess));
}
}
@ -496,7 +498,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
Map<String, String> properties = new HashMap<>();
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
CompileResult compileResult = compileToJs(singleTest(child), "test", configuration, outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.JAVASCRIPT, onSuccess);
TestRun run = prepareRun(configuration, child, compileResult, notifier, RunKind.JAVASCRIPT, onSuccess);
if (run != null) {
runs.add(run);
@ -515,7 +517,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
CompileResult compileResult = compileToC(singleTest(child), "test", configuration, outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.C, onSuccess);
TestRun run = prepareRun(configuration, child, compileResult, notifier, RunKind.C, onSuccess);
if (run != null) {
runs.add(run);
}
@ -524,7 +526,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
CompileResult compileResult = compileToWasm(singleTest(child), "test", configuration,
outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.WASM, onSuccess);
TestRun run = prepareRun(configuration, child, compileResult, notifier, RunKind.WASM, onSuccess);
if (run != null) {
runs.add(run);
}
@ -539,119 +541,167 @@ public class TeaVMTestRunner extends Runner implements Filterable {
onSuccess.accept(true);
}
private String[] getExpectedExceptions(MethodHolder method) {
AnnotationHolder annot = method.getAnnotations().get(JUNIT4_TEST);
if (annot == null) {
return new String[0];
}
AnnotationValue expected = annot.getValue("expected");
if (expected == null) {
return new String[0];
static String[] getExpectedExceptions(MethodReader method) {
AnnotationReader annot = method.getAnnotations().get(JUNIT4_TEST);
if (annot != null) {
AnnotationValue expected = annot.getValue("expected");
if (expected == null) {
return new String[0];
}
ValueType result = expected.getJavaClass();
return new String[] { ((ValueType.Object) result).getClassName() };
}
ValueType result = expected.getJavaClass();
return new String[] { ((ValueType.Object) result).getClassName() };
annot = method.getAnnotations().get(TESTNG_TEST);
if (annot != null) {
AnnotationValue expected = annot.getValue("expectedExceptions");
if (expected == null) {
return new String[0];
}
List<AnnotationValue> list = expected.getList();
String[] result = new String[list.size()];
for (int i = 0; i < list.size(); ++i) {
result[i] = ((ValueType.Object) list.get(i).getJavaClass()).getClassName();
}
return result;
}
return new String[0];
}
private boolean runInJvm(Method child, RunNotifier notifier, Set<Class<?>> expectedExceptions) {
Description description = describeChild(child);
Runner runner;
private boolean runInJvm(Method testMethod, RunNotifier notifier, String[] expectedExceptions) {
Description description = describeChild(testMethod);
Object instance;
try {
instance = testClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
instance = testClass.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException e) {
notifier.fireTestFailure(new Failure(description, e));
return false;
} catch (InvocationTargetException e) {
notifier.fireTestFailure(new Failure(description, e.getTargetException()));
return false;
}
Runner runner;
try {
runner = prepareJvmRunner(instance, testMethod, expectedExceptions);
} catch (Throwable e) {
notifier.fireTestFailure(new Failure(description, e));
return false;
}
if (!TestCase.class.isAssignableFrom(testClass)) {
runner = new JUnit4Runner(instance, child);
try {
runner.run(new Object[0]);
return true;
} catch (Throwable e) {
notifier.fireTestFailure(new Failure(description, e));
return false;
}
}
private Runner prepareJvmRunner(Object instance, Method testMethod, String[] expectedExceptions) throws Throwable {
Runner runner;
if (TestCase.class.isAssignableFrom(testClass)) {
runner = new JUnit3Runner((TestCase) instance, testMethod);
} else {
runner = new JUnit3Runner(instance);
((TestCase) instance).setName(child.getName());
runner = new SimpleMethodRunner(instance, testMethod);
}
if (expectedExceptions.length > 0) {
runner = new WithExpectedExceptionRunner(runner, expectedExceptions);
}
runner = wrapWithBeforeAndAfter(runner, instance);
runner = wrapWithDataProvider(runner, instance, testMethod);
return runner;
}
private Runner wrapWithBeforeAndAfter(Runner runner, Object instance) {
List<Class<?>> classes = new ArrayList<>();
Class<?> cls = instance.getClass();
while (cls != null) {
classes.add(cls);
cls = cls.getSuperclass();
}
List<Method> afterMethods = new ArrayList<>();
for (Class<?> c : classes) {
for (Method method : c.getMethods()) {
if (getAnnotation(method, JUNIT4_AFTER) != null || getAnnotation(method, TESTNG_AFTER) != null) {
afterMethods.add(method);
}
}
}
List<Method> beforeMethods = new ArrayList<>();
Collections.reverse(classes);
for (Class<?> c : classes) {
for (Method method : c.getMethods()) {
if (method.isAnnotationPresent(Before.class)) {
try {
method.invoke(instance);
} catch (InvocationTargetException e) {
notifier.fireTestFailure(new Failure(description, e.getTargetException()));
} catch (IllegalAccessException e) {
notifier.fireTestFailure(new Failure(description, e));
}
if (getAnnotation(method, JUNIT4_BEFORE) != null || getAnnotation(method, TESTNG_BEFORE) != null) {
beforeMethods.add(method);
}
}
}
if (beforeMethods.isEmpty() && afterMethods.isEmpty()) {
return runner;
}
return new WithBeforeAndAfterRunner(runner, instance, beforeMethods.toArray(new Method[0]),
afterMethods.toArray(new Method[0]));
}
private Runner wrapWithDataProvider(Runner runner, Object instance, Method testMethod) throws Throwable {
AnnotationHolder annot = getAnnotation(testMethod, TESTNG_TEST);
if (annot == null) {
return runner;
}
String providerName = annot.getValue("dataProvider").getString();
if (providerName.isEmpty()) {
return runner;
}
Method provider = null;
for (Method method : testMethod.getDeclaringClass().getDeclaredMethods()) {
AnnotationHolder providerAnnot = getAnnotation(method, TESTNG_PROVIDER);
if (providerAnnot != null && providerAnnot.getValue("name").getString().equals(providerName)) {
provider = method;
break;
}
}
Object data;
try {
boolean expectedCaught = false;
try {
runner.run();
} catch (Throwable e) {
boolean wasExpected = false;
for (Class<?> expected : expectedExceptions) {
if (expected.isInstance(e)) {
expectedCaught = true;
wasExpected = true;
}
}
if (!wasExpected) {
notifier.fireTestFailure(new Failure(description, e));
return false;
}
return false;
}
if (!expectedCaught && !expectedExceptions.isEmpty()) {
notifier.fireTestAssumptionFailed(new Failure(description,
new AssertionError("Expected exception was not thrown")));
return false;
}
return true;
} finally {
Collections.reverse(classes);
for (Class<?> c : classes) {
for (Method method : c.getMethods()) {
if (method.isAnnotationPresent(After.class)) {
try {
method.invoke(instance);
} catch (InvocationTargetException e) {
notifier.fireTestFailure(new Failure(description, e.getTargetException()));
} catch (IllegalAccessException e) {
notifier.fireTestFailure(new Failure(description, e));
}
}
}
}
provider.setAccessible(true);
data = provider.invoke(instance);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
return new WithDataProviderRunner(runner, data, testMethod.getParameterTypes());
}
interface Runner {
void run() throws Throwable;
void run(Object[] arguments) throws Throwable;
}
static class JUnit4Runner implements Runner {
static class SimpleMethodRunner implements Runner {
Object instance;
Method child;
Method testMethod;
JUnit4Runner(Object instance, Method child) {
SimpleMethodRunner(Object instance, Method testMethod) {
this.instance = instance;
this.child = child;
this.testMethod = testMethod;
}
@Override
public void run() throws Throwable {
public void run(Object[] arguments) throws Throwable {
try {
child.invoke(instance);
testMethod.invoke(instance, arguments);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
@ -659,20 +709,158 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
static class JUnit3Runner implements Runner {
Object instance;
TestCase instance;
Method testMethod;
JUnit3Runner(Object instance) {
JUnit3Runner(TestCase instance, Method testMethod) {
this.instance = instance;
this.testMethod = testMethod;
}
@Override
public void run() throws Throwable {
((TestCase) instance).runBare();
public void run(Object[] arguments) throws Throwable {
instance.setName(testMethod.getName());
instance.runBare();
}
}
private TestRun prepareRun(Method child, CompileResult result, RunNotifier notifier, RunKind kind,
Consumer<Boolean> onComplete) {
static class WithDataProviderRunner implements Runner {
Runner underlyingRunner;
Object data;
Class<?>[] types;
WithDataProviderRunner(Runner underlyingRunner, Object data, Class<?>[] types) {
this.underlyingRunner = underlyingRunner;
this.data = data;
this.types = types;
}
@Override
public void run(Object[] arguments) throws Throwable {
if (arguments.length > 0) {
throw new IllegalArgumentException("Expected 0 arguments");
}
if (data instanceof Iterator) {
runWithIteratorData((Iterator<?>) data);
} else {
runWithArrayData((Object[][]) data);
}
}
private void runWithArrayData(Object[][] data) throws Throwable {
for (int i = 0; i < data.length; ++i) {
runWithDataRow(data[i]);
}
}
private void runWithIteratorData(Iterator<?> data) throws Throwable {
while (data.hasNext()) {
runWithDataRow((Object[]) data.next());
}
}
private void runWithDataRow(Object[] dataRow) throws Throwable {
Object[] args = dataRow.clone();
for (int j = 0; j < args.length; ++j) {
args[j] = convert(args[j], types[j]);
}
underlyingRunner.run(args);
}
private Object convert(Object value, Class<?> type) {
if (type == byte.class) {
value = ((Number) value).byteValue();
} else if (type == short.class) {
value = ((Number) value).shortValue();
} else if (type == int.class) {
value = ((Number) value).intValue();
} else if (type == long.class) {
value = ((Number) value).longValue();
} else if (type == float.class) {
value = ((Number) value).floatValue();
} else if (type == double.class) {
value = ((Number) value).doubleValue();
}
return value;
}
}
static class WithExpectedExceptionRunner implements Runner {
private Runner underlyingRunner;
private String[] expectedExceptions;
WithExpectedExceptionRunner(Runner underlyingRunner, String[] expectedExceptions) {
this.underlyingRunner = underlyingRunner;
this.expectedExceptions = expectedExceptions;
}
@Override
public void run(Object[] arguments) throws Throwable {
boolean caught = false;
try {
underlyingRunner.run(arguments);
} catch (Exception e) {
for (String expected : expectedExceptions) {
if (isSubtype(e.getClass(), expected)) {
caught = true;
break;
}
}
if (!caught) {
throw e;
}
}
if (!caught) {
throw new AssertionError("Expected exception not thrown");
}
}
private boolean isSubtype(Class<?> cls, String superType) {
while (cls != Throwable.class) {
if (cls.getName().equals(superType)) {
return true;
}
cls = cls.getSuperclass();
}
return false;
}
}
static class WithBeforeAndAfterRunner implements Runner {
private Runner underlyingRunner;
private Object instance;
private Method[] beforeMethods;
private Method[] afterMethods;
WithBeforeAndAfterRunner(Runner underlyingRunner, Object instance, Method[] beforeMethods,
Method[] afterMethods) {
this.underlyingRunner = underlyingRunner;
this.instance = instance;
this.beforeMethods = beforeMethods;
this.afterMethods = afterMethods;
}
@Override
public void run(Object[] arguments) throws Throwable {
for (Method method : beforeMethods) {
try {
method.invoke(instance);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
try {
underlyingRunner.run(arguments);
} finally {
for (Method method : afterMethods) {
method.invoke(instance);
}
}
}
}
private TestRun prepareRun(TeaVMTestConfiguration<?> configuration, Method child, CompileResult result,
RunNotifier notifier, RunKind kind, Consumer<Boolean> onComplete) {
Description description = describeChild(child);
if (!result.success) {
@ -682,11 +870,11 @@ public class TeaVMTestRunner extends Runner implements Filterable {
return null;
}
return createTestRun(result.file, child, kind, null, notifier, onComplete);
return createTestRun(configuration, result.file, child, kind, null, notifier, onComplete);
}
private TestRun createTestRun(File file, Method child, RunKind kind, String argument, RunNotifier notifier,
Consumer<Boolean> onComplete) {
private TestRun createTestRun(TeaVMTestConfiguration<?> configuration, File file, Method child, RunKind kind,
String argument, RunNotifier notifier, Consumer<Boolean> onComplete) {
Description description = describeChild(child);
TestRunCallback callback = new TestRunCallback() {
@ -702,8 +890,16 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
};
return new TestRun(file.getParentFile(), child, description, file.getName(), kind,
argument, callback);
return new TestRun(generateName(child.getName(), configuration), file.getParentFile(), child, description,
file.getName(), kind, argument, callback);
}
private String generateName(String baseName, TeaVMTestConfiguration<?> configuration) {
String suffix = configuration.getSuffix();
if (!suffix.isEmpty()) {
baseName = baseName + " (" + suffix + ")";
}
return baseName;
}
private Failure createFailure(Description description, CompileResult result) {
@ -854,6 +1050,9 @@ public class TeaVMTestRunner extends Runner implements Filterable {
vm.setProperties(properties);
List<MethodReference> methodReferences = new ArrayList<>();
for (Method method : methods) {
if (isIgnored(method)) {
continue;
}
ClassHolder classHolder = classSource.get(method.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
methodReferences.add(methodHolder.getReference());
@ -862,6 +1061,31 @@ public class TeaVMTestRunner extends Runner implements Filterable {
};
}
private boolean isIgnored(Method method) {
return getAnnotation(method, JUNIT4_IGNORE) != null || getAnnotation(method, TESTNG_IGNORE) != null;
}
private AnnotationHolder getAnnotation(Method method, String name) {
ClassHolder cls = classSource.get(method.getDeclaringClass().getName());
if (cls == null) {
return null;
}
MethodDescriptor descriptor = getDescriptor(method);
MethodHolder methodHolder = cls.getMethod(descriptor);
if (methodHolder == null) {
return null;
}
return methodHolder.getAnnotations().get(name);
}
private AnnotationHolder getClassAnnotation(Method method, String name) {
ClassHolder cls = classSource.get(method.getDeclaringClass().getName());
if (cls == null) {
return null;
}
return cls.getAnnotations().get(name);
}
private <T extends TeaVMTarget> CompileResult compile(TeaVMTestConfiguration<T> configuration,
Supplier<T> targetSupplier, String entryPoint, File path, String extension,
CompilePostProcessor postBuild, boolean separateDir,
@ -1096,7 +1320,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
Writer writer = new OutputStreamWriter(bufferedOutput)) {
writer.write("[\n");
boolean first = true;
for (TestRun run : runsInCurrentClass) {
for (TestRun run : runsInCurrentClass.toArray(new TestRun[0])) {
if (!first) {
writer.write(",\n");
}
@ -1114,6 +1338,9 @@ public class TeaVMTestRunner extends Runner implements Filterable {
writer.write(" \"argument\": ");
writeJsonString(writer, run.getArgument());
}
writer.write(",\n");
writer.write(" \"name\": ");
writeJsonString(writer, run.getName());
writer.write("\n }");
}
writer.write("\n]");

View File

@ -15,32 +15,46 @@
*/
package org.teavm.junit;
import java.util.ArrayList;
import java.util.List;
final class TestEntryPoint {
private static Object testCase;
private TestEntryPoint() {
}
public static void run(String name) throws Exception {
before();
try {
launchTest(name);
} finally {
public static void run(String name) throws Throwable {
List<Launcher> launchers = new ArrayList<>();
testCase = createTestCase();
launchers(name, launchers);
for (Launcher launcher : launchers) {
before();
try {
after();
} catch (Throwable e) {
e.printStackTrace();
launcher.launch(testCase);
} finally {
try {
after();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
private static native Object createTestCase();
private static native void before();
private static native void launchTest(String name) throws Exception;
private static native void launchers(String name, List<Launcher> result) throws Throwable;
private static native void after();
public static void main(String[] args) throws Throwable {
run(args.length == 1 ? args[0] : null);
}
interface Launcher {
void launch(Object testCase) throws Throwable;
}
}

View File

@ -20,7 +20,10 @@ import static org.teavm.junit.TeaVMTestRunner.JUNIT3_BASE_CLASS;
import static org.teavm.junit.TeaVMTestRunner.JUNIT3_BEFORE;
import static org.teavm.junit.TeaVMTestRunner.JUNIT4_AFTER;
import static org.teavm.junit.TeaVMTestRunner.JUNIT4_BEFORE;
import static org.teavm.junit.TeaVMTestRunner.JUNIT4_TEST;
import static org.teavm.junit.TeaVMTestRunner.TESTNG_AFTER;
import static org.teavm.junit.TeaVMTestRunner.TESTNG_BEFORE;
import static org.teavm.junit.TeaVMTestRunner.TESTNG_PROVIDER;
import static org.teavm.junit.TeaVMTestRunner.TESTNG_TEST;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -34,12 +37,14 @@ import org.teavm.model.ClassHolderTransformerContext;
import org.teavm.model.ClassReader;
import org.teavm.model.ClassReaderSource;
import org.teavm.model.ElementModifier;
import org.teavm.model.FieldHolder;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.TryCatchBlock;
import org.teavm.model.ValueType;
import org.teavm.model.emit.PhiEmitter;
import org.teavm.model.emit.ProgramEmitter;
import org.teavm.model.emit.ValueEmitter;
import org.teavm.vm.spi.TeaVMHost;
@ -47,6 +52,7 @@ import org.teavm.vm.spi.TeaVMPlugin;
abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
private String testClassName;
private int suffixGenerator;
TestEntryPointTransformer(String testClassName) {
this.testClassName = testClassName;
@ -60,18 +66,23 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
@Override
public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) {
if (cls.getName().equals(TestEntryPoint.class.getName())) {
suffixGenerator = 0;
for (MethodHolder method : cls.getMethods()) {
switch (method.getName()) {
case "launchTest":
method.setProgram(generateLaunchProgram(method, context.getHierarchy()));
case "createTestCase":
generateCreateTestCaseProgram(method, context.getHierarchy());
method.getModifiers().remove(ElementModifier.NATIVE);
break;
case "launchers":
generateLaunchProgram(method, context);
method.getModifiers().remove(ElementModifier.NATIVE);
break;
case "before":
method.setProgram(generateBeforeProgram(method, context.getHierarchy()));
generateBeforeProgram(method, context.getHierarchy());
method.getModifiers().remove(ElementModifier.NATIVE);
break;
case "after":
method.setProgram(generateAfterProgram(method, context.getHierarchy()));
generateAfterProgram(method, context.getHierarchy());
method.getModifiers().remove(ElementModifier.NATIVE);
break;
}
@ -79,14 +90,13 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
}
}
private Program generateBeforeProgram(MethodHolder method, ClassHierarchy hierarchy) {
private void generateCreateTestCaseProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
pe.construct(testClassName).cast(Object.class).returnValue();
}
private void generateBeforeProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
ValueEmitter testCaseInitVar = pe.getField(TestEntryPoint.class, "testCase", Object.class);
pe.when(testCaseInitVar.isNull())
.thenDo(() -> {
pe.setField(TestEntryPoint.class, "testCase",
pe.construct(testClassName).cast(Object.class));
});
ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class);
if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testClassName, false)) {
@ -97,11 +107,11 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
Collections.reverse(classes);
classes.stream()
.flatMap(cls -> cls.getMethods().stream())
.filter(m -> m.getAnnotations().get(JUNIT4_BEFORE) != null)
.filter(m -> m.getAnnotations().get(JUNIT4_BEFORE) != null
|| m.getAnnotations().get(TESTNG_BEFORE) != null)
.forEach(m -> testCaseVar.cast(ValueType.object(m.getOwnerName())).invokeVirtual(m.getReference()));
pe.exit();
return pe.getProgram();
}
private Program generateAfterProgram(MethodHolder method, ClassHierarchy hierarchy) {
@ -111,7 +121,8 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testClassName);
classes.stream()
.flatMap(cls -> cls.getMethods().stream())
.filter(m -> m.getAnnotations().get(JUNIT4_AFTER) != null)
.filter(m -> m.getAnnotations().get(JUNIT4_AFTER) != null
|| m.getAnnotations().get(TESTNG_AFTER) != null)
.forEach(m -> testCaseVar.cast(ValueType.object(m.getOwnerName())).invokeVirtual(m.getReference()));
if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testClassName, false)) {
@ -135,23 +146,181 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
return result;
}
protected abstract Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy);
protected abstract void generateLaunchProgram(MethodHolder method, ClassHolderTransformerContext context);
protected final void generateSingleMethodLaunchProgram(MethodReference testMethod,
ClassHierarchy hierarchy, ProgramEmitter pe) {
pe.getField(TestEntryPoint.class, "testCase", Object.class)
.cast(ValueType.object(testMethod.getClassName()))
.invokeSpecial(testMethod);
ClassHolderTransformerContext context, ProgramEmitter pe) {
ClassHolder launcherClass = generateLauncherClass(testMethod, context.getHierarchy());
context.submit(launcherClass);
ValueEmitter list = pe.var(2, List.class);
MethodReader testMethodReader = context.getHierarchy().getClassSource().resolve(testMethod);
AnnotationReader testNgAnnot = testMethodReader.getAnnotations().get(TESTNG_TEST);
if (testNgAnnot != null) {
AnnotationValue dataProviderValue = testNgAnnot.getValue("dataProvider");
if (dataProviderValue != null) {
generateAddLaunchersWithProvider(testMethodReader, context.getHierarchy(), pe, list,
dataProviderValue.getString(), launcherClass.getName());
return;
}
}
list.invokeVirtual("add", boolean.class, pe.construct(launcherClass.getName()).cast(Object.class));
pe.exit();
}
private void generateAddLaunchersWithProvider(MethodReader testMethodReader, ClassHierarchy hierarchy,
ProgramEmitter pe, ValueEmitter list, String providerName, String launcherClassName) {
ClassReader owningClass = hierarchy.getClassSource().get(testMethodReader.getOwnerName());
MethodReader providerMethod = null;
for (MethodReader method : owningClass.getMethods()) {
AnnotationReader annot = method.getAnnotations().get(TESTNG_PROVIDER);
if (annot != null && annot.getValue("name").getString().equals(providerName)) {
providerMethod = method;
break;
}
}
ValueEmitter data = pe.getField(TestEntryPoint.class, "testCase", Object.class)
.cast(ValueType.object(testMethodReader.getOwnerName()))
.invokeSpecial(providerMethod.getReference());
if (data.getType() instanceof ValueType.Array) {
generateAddLaunchersWithProviderArray(testMethodReader, pe, list, data, launcherClassName);
} else {
generateAddLaunchersWithProviderIterator(testMethodReader, pe, list, data, launcherClassName);
}
}
private void generateAddLaunchersWithProviderArray(MethodReader testMethodReader, ProgramEmitter pe,
ValueEmitter list, ValueEmitter data, String launcherClassName) {
ValueEmitter size = data.arrayLength();
BasicBlock loopHead = pe.getProgram().createBasicBlock();
BasicBlock loopBody = pe.getProgram().createBasicBlock();
BasicBlock loopExit = pe.getProgram().createBasicBlock();
PhiEmitter index = pe.phi(int.class, loopHead);
pe.constant(0).propagateTo(index);
pe.jump(loopHead);
pe.enter(loopHead);
pe.when(index.getValue().isLessThan(size))
.thenDo(() -> pe.jump(loopBody))
.elseDo(() -> pe.jump(loopExit));
pe.enter(loopBody);
ValueEmitter dataRow = data.getElement(index.getValue());
generateAddLauncherWithData(testMethodReader, pe, list, dataRow, launcherClassName);
index.getValue().add(1).propagateTo(index);
pe.jump(loopHead);
pe.enter(loopExit);
pe.exit();
}
private void generateAddLaunchersWithProviderIterator(MethodReader testMethodReader, ProgramEmitter pe,
ValueEmitter list, ValueEmitter data, String launcherClassName) {
BasicBlock loopHead = pe.getProgram().createBasicBlock();
BasicBlock loopBody = pe.getProgram().createBasicBlock();
BasicBlock loopExit = pe.getProgram().createBasicBlock();
pe.jump(loopHead);
pe.enter(loopHead);
pe.when(data.invokeVirtual("hasNext", boolean.class).isTrue())
.thenDo(() -> pe.jump(loopBody))
.elseDo(() -> pe.jump(loopExit));
pe.enter(loopBody);
ValueEmitter dataRow = data.invokeVirtual("next", Object.class).cast(Object[].class);
generateAddLauncherWithData(testMethodReader, pe, list, dataRow, launcherClassName);
pe.jump(loopHead);
pe.enter(loopExit);
pe.exit();
}
private void generateAddLauncherWithData(MethodReader testMethodReader, ProgramEmitter pe, ValueEmitter list,
ValueEmitter dataRow, String launcherClassName) {
List<ValueEmitter> arguments = new ArrayList<>();
for (int i = 0; i < testMethodReader.parameterCount(); ++i) {
ValueType type = testMethodReader.parameterType(i);
arguments.add(convertArgument(dataRow.getElement(i), type));
}
list.invokeVirtual("add", boolean.class, pe.construct(launcherClassName,
arguments.toArray(new ValueEmitter[0])).cast(Object.class));
}
private ValueEmitter convertArgument(ValueEmitter value, ValueType type) {
if (type instanceof ValueType.Primitive) {
switch (((ValueType.Primitive) type).getKind()) {
case BOOLEAN:
return value.cast(Boolean.class).invokeVirtual("booleanValue", boolean.class);
case CHARACTER:
return value.cast(Character.class).invokeVirtual("charValue", char.class);
case BYTE:
return value.cast(Number.class).invokeVirtual("byteValue", byte.class);
case SHORT:
return value.cast(Number.class).invokeVirtual("shortValue", byte.class);
case INTEGER:
return value.cast(Number.class).invokeVirtual("intValue", int.class);
case LONG:
return value.cast(Number.class).invokeVirtual("longValue", long.class);
case FLOAT:
return value.cast(Number.class).invokeVirtual("floatValue", float.class);
case DOUBLE:
return value.cast(Number.class).invokeVirtual("doubleValue", double.class);
}
}
return value.cast(type);
}
private ClassHolder generateLauncherClass(MethodReference testMethod, ClassHierarchy hierarchy) {
ClassHolder cls = new ClassHolder(TestEntryPoint.Launcher.class.getName() + "Impl" + suffixGenerator++);
cls.setParent("java.lang.Object");
cls.getInterfaces().add(TestEntryPoint.Launcher.class.getName());
MethodHolder constructor = new MethodHolder("<init>", testMethod.getSignature());
cls.addMethod(constructor);
ProgramEmitter pe = ProgramEmitter.create(constructor, hierarchy);
pe.invoke(Object.class, "<init>", void.class);
ValueEmitter self = pe.var(0, ValueType.object(cls.getName()));
for (int i = 0; i < testMethod.parameterCount(); ++i) {
FieldHolder paramField = new FieldHolder("param_" + i);
paramField.setType(testMethod.parameterType(i));
cls.addField(paramField);
self.setField(paramField.getName(), pe.var(i + 1, testMethod.parameterType(i)));
}
pe.exit();
MethodHolder launchMethod = new MethodHolder("launch", ValueType.parse(Object.class), ValueType.VOID);
cls.addMethod(launchMethod);
pe = ProgramEmitter.create(launchMethod, hierarchy);
List<ValueEmitter> arguments = new ArrayList<>();
self = pe.var(0, ValueType.object(cls.getName()));
for (int i = 0; i < testMethod.parameterCount(); ++i) {
arguments.add(self.getField("param_" + i, testMethod.parameterType(i)));
}
generateRunMethodOnce(testMethod, hierarchy, pe, pe.var(1, Object.class), arguments);
pe.exit();
return cls;
}
private void generateRunMethodOnce(MethodReference testMethod, ClassHierarchy hierarchy, ProgramEmitter pe,
ValueEmitter testCase, List<ValueEmitter> arguments) {
testCase.cast(ValueType.object(testMethod.getClassName()))
.invokeSpecial(testMethod, arguments.toArray(new ValueEmitter[0]));
MethodReader testMethodReader = hierarchy.getClassSource().resolve(testMethod);
AnnotationReader testAnnotation = testMethodReader.getAnnotations().get(JUNIT4_TEST);
AnnotationValue throwsValue = testAnnotation != null ? testAnnotation.getValue("expected") : null;
if (throwsValue != null) {
String[] expectedExceptions = TeaVMTestRunner.getExpectedExceptions(testMethodReader);
if (expectedExceptions.length != 0) {
BasicBlock handler = pe.getProgram().createBasicBlock();
TryCatchBlock tryCatch = new TryCatchBlock();
tryCatch.setExceptionType(((ValueType.Object) throwsValue.getJavaClass()).getClassName());
tryCatch.setHandler(handler);
pe.getBlock().getTryCatchBlocks().add(tryCatch);
for (String exceptionType : expectedExceptions) {
TryCatchBlock tryCatch = new TryCatchBlock();
tryCatch.setExceptionType(exceptionType);
tryCatch.setHandler(handler);
pe.getBlock().getTryCatchBlocks().add(tryCatch);
}
BasicBlock nextBlock = pe.getProgram().createBasicBlock();
pe.jump(nextBlock);
@ -159,9 +328,6 @@ abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaV
pe.construct(AssertionError.class, pe.constant("Expected exception not thrown")).raise();
pe.enter(handler);
pe.exit();
} else {
pe.exit();
}
}
}

View File

@ -15,10 +15,9 @@
*/
package org.teavm.junit;
import org.teavm.model.ClassHierarchy;
import org.teavm.model.ClassHolderTransformerContext;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.emit.ProgramEmitter;
class TestEntryPointTransformerForSingleMethod extends TestEntryPointTransformer {
@ -30,9 +29,8 @@ class TestEntryPointTransformerForSingleMethod extends TestEntryPointTransformer
}
@Override
protected Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
generateSingleMethodLaunchProgram(testMethod, hierarchy, pe);
return pe.getProgram();
protected void generateLaunchProgram(MethodHolder method, ClassHolderTransformerContext context) {
ProgramEmitter pe = ProgramEmitter.create(method, context.getHierarchy());
generateSingleMethodLaunchProgram(testMethod, context, pe);
}
}

View File

@ -16,10 +16,9 @@
package org.teavm.junit;
import java.util.List;
import org.teavm.model.ClassHierarchy;
import org.teavm.model.ClassHolderTransformerContext;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.emit.ForkEmitter;
import org.teavm.model.emit.ProgramEmitter;
import org.teavm.model.emit.ValueEmitter;
@ -34,8 +33,8 @@ class TestEntryPointTransformerForWholeClass extends TestEntryPointTransformer {
}
@Override
protected Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
protected void generateLaunchProgram(MethodHolder method, ClassHolderTransformerContext context) {
ProgramEmitter pe = ProgramEmitter.create(method, context.getHierarchy());
ValueEmitter testName = pe.var(1, String.class);
for (MethodReference testMethod : testMethods) {
@ -45,13 +44,11 @@ class TestEntryPointTransformerForWholeClass extends TestEntryPointTransformer {
pe.enter(pe.getProgram().createBasicBlock());
fork.setThen(pe.getBlock());
generateSingleMethodLaunchProgram(testMethod, hierarchy, pe);
generateSingleMethodLaunchProgram(testMethod, context, pe);
pe.enter(pe.getProgram().createBasicBlock());
fork.setElse(pe.getBlock());
}
pe.construct(IllegalArgumentException.class, pe.constant("Invalid test name")).raise();
return pe.getProgram();
}
}

View File

@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import org.junit.runner.Description;
class TestRun {
private String name;
private File baseDirectory;
private Method method;
private Description description;
@ -28,8 +29,9 @@ class TestRun {
private TestRunCallback callback;
private String argument;
TestRun(File baseDirectory, Method method, Description description, String fileName, RunKind kind,
TestRun(String name, File baseDirectory, Method method, Description description, String fileName, RunKind kind,
String argument, TestRunCallback callback) {
this.name = name;
this.baseDirectory = baseDirectory;
this.method = method;
this.description = description;
@ -39,6 +41,10 @@ class TestRun {
this.callback = callback;
}
public String getName() {
return name;
}
public File getBaseDirectory() {
return baseDirectory;
}