diff --git a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java index 0fd809a65..e09675e0a 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java +++ b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java @@ -140,5 +140,10 @@ class DependencyClassSource implements ClassHolderSource { public boolean isStrict() { return strict; } + + @Override + public void submit(ClassHolder cls) { + DependencyClassSource.this.submit(cls); + } }; } diff --git a/core/src/main/java/org/teavm/dependency/VirtualCallConsumer.java b/core/src/main/java/org/teavm/dependency/VirtualCallConsumer.java index f3831a304..ce0e1a4f2 100644 --- a/core/src/main/java/org/teavm/dependency/VirtualCallConsumer.java +++ b/core/src/main/java/org/teavm/dependency/VirtualCallConsumer.java @@ -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"; } diff --git a/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java b/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java index c5b4ad27d..936864d3a 100644 --- a/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java +++ b/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java @@ -28,4 +28,6 @@ public interface ClassHolderTransformerContext { boolean isObfuscated(); boolean isStrict(); + + void submit(ClassHolder cls); } diff --git a/pom.xml b/pom.xml index 3bbf8fbc6..a3278be96 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ 11 1.7.11 3.8.0 + 7.1.0 4.13.2 1.4 @@ -212,6 +213,11 @@ rhino ${rhino.version} + + org.testng + testng + ${testng.version} + commons-cli commons-cli diff --git a/tools/junit/pom.xml b/tools/junit/pom.xml index cbb120627..4b1ba85e3 100644 --- a/tools/junit/pom.xml +++ b/tools/junit/pom.xml @@ -35,6 +35,11 @@ junit provided + + org.testng + testng + provided + org.teavm teavm-tooling diff --git a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java index 6ffca38a6..8eba20ab4 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java +++ b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java @@ -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> 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 properties = new HashMap<>(); for (TeaVMTestConfiguration 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 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 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 properties = new HashMap<>(); for (TeaVMTestConfiguration 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 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 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 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> 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> classes = new ArrayList<>(); Class cls = instance.getClass(); while (cls != null) { classes.add(cls); cls = cls.getSuperclass(); } + + List 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 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 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 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 onComplete) { + private TestRun createTestRun(TeaVMTestConfiguration configuration, File file, Method child, RunKind kind, + String argument, RunNotifier notifier, Consumer 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 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 CompileResult compile(TeaVMTestConfiguration configuration, Supplier 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]"); diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java index 8187aa05b..6243594e9 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java @@ -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 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 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; + } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java index 326010ae9..6ed01da7f 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java @@ -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 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 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("", testMethod.getSignature()); + cls.addMethod(constructor); + ProgramEmitter pe = ProgramEmitter.create(constructor, hierarchy); + pe.invoke(Object.class, "", 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 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 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(); } } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java index 713ee8cd3..796dabd7f 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java @@ -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); } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java index 4e4d6665b..2ea15c1e7 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java @@ -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(); } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestRun.java b/tools/junit/src/main/java/org/teavm/junit/TestRun.java index fb8f4dfd8..e7f460dc6 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestRun.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestRun.java @@ -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; }