Improving JUnit test runner

This commit is contained in:
Alexey Andreev 2016-02-23 21:40:51 +03:00
parent 2b3cba69bc
commit d103306c3e
31 changed files with 636 additions and 115 deletions

View File

@ -20,6 +20,13 @@
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Eclipse" />
<excludeFromCompile>
<directory url="file://$PROJECT_DIR$/tools/maven/webapp/src/main/resources/archetype-resources" includeSubdirectories="true" />
</excludeFromCompile>

View File

@ -71,13 +71,6 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -83,43 +83,6 @@
<build>
<plugins>
<plugin>
<groupId>org.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>generate-javascript-tests</id>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<minifying>false</minifying>
<scanDependencies>true</scanDependencies>
<wildcards>
<wildcard>org.teavm.classlib.**.*Test</wildcard>
<wildcard>org.teavm.jso.**.*Test</wildcard>
<wildcard>org.teavm.platform.metadata.*Test</wildcard>
</wildcards>
<properties>
<java.util.Locale.available>en, en_US, en_GB, ru, ru_RU</java.util.Locale.available>
</properties>
<incremental>${teavm.test.incremental}</incremental>
</configuration>
</execution>
<execution>
<id>run-javascript-tests</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<skip>${teavm.test.skip}</skip>
<numThreads>${teavm.test.threads}</numThreads>
<seleniumURL>${teavm.test.selenium}</seleniumURL>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>

View File

@ -23,9 +23,12 @@ import java.text.ParseException;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMProperties;
import org.teavm.junit.TeaVMProperty;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@TeaVMProperties(@TeaVMProperty(key = "java.util.Locale.available", value = "en, en_US, en_GB, ru, ru_RU"))
public class DecimalFormatParseTest {
private static DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH);

View File

@ -25,9 +25,12 @@ import java.util.Currency;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMProperties;
import org.teavm.junit.TeaVMProperty;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@TeaVMProperties(@TeaVMProperty(key = "java.util.Locale.available", value = "en, en_US, en_GB, ru, ru_RU"))
public class DecimalFormatTest {
private static DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH);

View File

@ -7,6 +7,7 @@ import java.util.Locale;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@ -32,7 +33,7 @@ public class NumberFormatTest {
}
@Test
@Ignore
@SkipJVM
public void formatsPercent() {
NumberFormat format = NumberFormat.getPercentInstance(new Locale("en", "US"));
assertEquals("12,345,679%", format.format(123456.789123));

View File

@ -21,9 +21,13 @@ import java.util.Locale;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMProperties;
import org.teavm.junit.TeaVMProperty;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@TeaVMProperties(@TeaVMProperty(key = "java.util.Locale.available", value = "en, en_US, en_GB, ru, ru_RU"))
public class CurrencyTest {
@Test
public void findsByCode() {
@ -46,8 +50,7 @@ public class CurrencyTest {
}
@Test
@Ignore
// It seems that JDK can't translate currency names into Russian
@SkipJVM
public void getsDisplayName() {
Locale russian = new Locale("ru");
Locale english = new Locale("en");
@ -64,8 +67,7 @@ public class CurrencyTest {
}
@Test
@Ignore
// It seems that JDK does not know about currency symbols
@SkipJVM
public void getsSymbol() {
Locale russian = new Locale("ru");
Locale english = new Locale("en");

View File

@ -19,9 +19,12 @@ import static org.junit.Assert.*;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMProperties;
import org.teavm.junit.TeaVMProperty;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@TeaVMProperties(@TeaVMProperty(key = "java.util.Locale.available", value = "en, en_US, en_GB, ru, ru_RU"))
public class LocaleTest {
@Test
public void availableLocalesFound() {

View File

@ -15,14 +15,6 @@
*/
package org.teavm.classlib.java.util;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMTestRunner;
/**
*
* @author Alexey Andreev
*/
@RunWith(TeaVMTestRunner.class)
public class TestServiceImpl implements TestService {
private int counter;

View File

@ -22,9 +22,11 @@ import org.teavm.jso.JSBody;
import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class AnnotationsTest {
@Test
public void staticBodyWorks() {

View File

@ -22,9 +22,11 @@ import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.core.JSString;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class ConversionTest {
@Test
public void convertsPrimitivesToJavaScript() {

View File

@ -21,9 +21,11 @@ import org.junit.runner.RunWith;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class FunctorTest {
@Test
public void functorPassed() {

View File

@ -20,13 +20,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
/**
*
* @author Alexey Andreev
*/
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class ImplementationTest {
@Test
public void respectsPrecedence() {
@ -35,10 +33,10 @@ public class ImplementationTest {
}
@JSBody(params = { "a", "b" }, script = "return a + b;")
static final native int add(int a, int b);
static native int add(int a, int b);
@JSBody(params = { "a", "b" }, script = "return a * b;")
static final native int mul(int a, int b);
static native int mul(int a, int b);
@Test
public void inliningUsageCounterWorksProperly() {

View File

@ -20,9 +20,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class JavaInvocationTest {
@Test
public void callStaticMethod() {

View File

@ -18,6 +18,7 @@ package org.teavm.platform.metadata;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
/**
@ -25,6 +26,7 @@ import org.teavm.junit.TeaVMTestRunner;
* @author Alexey Andreev
*/
@RunWith(TeaVMTestRunner.class)
@SkipJVM
public class MetadataGeneratorTest {
@MetadataProvider(TestResourceGenerator.class)
private native TestResource getNull();

View File

@ -22,17 +22,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.diagnostics.Problem;
import org.teavm.jso.JSBody;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;
import org.teavm.vm.TeaVM;
import org.teavm.vm.TeaVMBuilder;
/**
*
* @author Alexey Andreev
*/
@RunWith(TeaVMTestRunner.class)
public class JSOTest {
@Test
public void reportsAboutWrongParameterOfJSBody() {

View File

@ -55,7 +55,7 @@ public final class TeaVMProblemRenderer {
}
}
private static void renderCallStack(CallGraph cg, CallLocation location, StringBuilder sb) {
public static void renderCallStack(CallGraph cg, CallLocation location, StringBuilder sb) {
if (location == null) {
return;
}
@ -76,7 +76,7 @@ public final class TeaVMProblemRenderer {
}
}
private static void renderCallLocation(MethodReference method, InstructionLocation location, StringBuilder sb) {
public static void renderCallLocation(MethodReference method, InstructionLocation location, StringBuilder sb) {
if (method != null) {
sb.append(method.getClassName() + "." + method.getName());
} else {

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
/**
*
* @author Alexey Andreev
*/
final class ExceptionHelper {
private ExceptionHelper() {
}
public static String showException(Throwable e) {
return e.getMessage();
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface SkipJVM {
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface TeaVMProperties {
TeaVMProperty[] value();
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
public @interface TeaVMProperty {
String key();
String value();
}

View File

@ -16,72 +16,258 @@
package org.teavm.junit;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;
import org.teavm.callgraph.CallGraph;
import org.teavm.diagnostics.DefaultProblemTextConsumer;
import org.teavm.diagnostics.Problem;
import org.teavm.model.ClassHolder;
import org.teavm.model.ClassHolderSource;
import org.teavm.model.MethodDescriptor;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.PreOptimizingClassHolderSource;
import org.teavm.model.ValueType;
import org.teavm.parsing.ClasspathClassHolderSource;
import org.teavm.testing.JUnitTestAdapter;
import org.teavm.testing.TestAdapter;
import org.teavm.tooling.TeaVMProblemRenderer;
import org.teavm.tooling.testing.TeaVMTestTool;
import org.teavm.vm.DirectoryBuildTarget;
import org.teavm.vm.TeaVM;
import org.teavm.vm.TeaVMBuilder;
public class TeaVMTestRunner extends Runner {
public class TeaVMTestRunner extends ParentRunner<Method> {
private static final String PATH_PARAM = "teavm.junit.target";
private Class<?> testClass;
private Description description;
private ClassHolder classHolder;
private ClassLoader classLoader;
private ClassHolderSource classSource;
private static Map<ClassLoader, ClassHolderSource> classSources = new WeakHashMap<>();
private File outputDir;
private TestAdapter testAdapter = new JUnitTestAdapter();
private Map<Method, Description> descriptions = new HashMap<>();
public TeaVMTestRunner(Class<?> testClass) {
this.testClass = testClass;
public TeaVMTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
classLoader = TeaVMTestRunner.class.getClassLoader();
classSource = getClassSource(classLoader);
classHolder = classSource.get(testClass.getName());
String outputPath = System.getProperty(PATH_PARAM);
if (outputPath != null) {
outputDir = new File(outputPath);
}
}
@Override
public Description getDescription() {
if (description == null) {
description = Description.createSuiteDescription(testClass);
for (Method method : testClass.getMethods()) {
if (method.getParameterCount() == 0 && method.getReturnType() == void.class
&& method.isAnnotationPresent(Test.class)) {
Description testDescription = Description.createTestDescription(testClass, method.getName());
description.addChild(testDescription);
protected List<Method> getChildren() {
List<Method> children = new ArrayList<>();
for (Method method : getTestClass().getJavaClass().getDeclaredMethods()) {
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
if (testAdapter.acceptMethod(methodHolder)) {
children.add(method);
}
}
return children;
}
@Override
protected Description describeChild(Method child) {
return descriptions.computeIfAbsent(child, method -> Description.createTestDescription(
getTestClass().getJavaClass(), method.getName()));
}
@Override
protected void runChild(Method child, RunNotifier notifier) {
notifier.fireTestStarted(describeChild(child));
boolean run = false;
boolean success = true;
if (outputDir != null) {
run = true;
success &= runInTeaVM(child, notifier);
}
if (success && !child.isAnnotationPresent(SkipJVM.class)
&& !child.getDeclaringClass().isAnnotationPresent(SkipJVM.class)) {
run = true;
success &= runInJvm(child, notifier);
}
if (!run) {
notifier.fireTestIgnored(describeChild(child));
}
notifier.fireTestFinished(describeChild(child));
}
private boolean runInJvm(Method child, RunNotifier notifier) {
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(child));
Set<Class<?>> expectedExceptions = new HashSet<>();
for (String exceptionName : testAdapter.getExpectedExceptions(methodHolder)) {
try {
expectedExceptions.add(Class.forName(exceptionName, false, classLoader));
} catch (ClassNotFoundException e) {
notifier.fireTestFailure(new Failure(describeChild(child), e));
return false;
}
}
Object instance;
try {
instance = getTestClass().getJavaClass().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
notifier.fireTestFailure(new Failure(describeChild(child), e));
return false;
}
boolean expectedCaught = false;
try {
child.invoke(instance);
} catch (IllegalAccessException e) {
notifier.fireTestFailure(new Failure(describeChild(child), e));
return false;
} catch (InvocationTargetException e) {
boolean wasExpected = false;
for (Class<?> expected : expectedExceptions) {
if (expected.isInstance(e.getTargetException())) {
expectedCaught = true;
wasExpected = true;
}
}
if (!wasExpected) {
notifier.fireTestFailure(new Failure(describeChild(child), e.getTargetException()));
return false;
}
}
return description;
if (!expectedCaught && !expectedExceptions.isEmpty()) {
notifier.fireTestAssumptionFailed(new Failure(describeChild(child),
new AssertionError("Expected exception was not thrown")));
return false;
}
return true;
}
@Override
public void run(RunNotifier notifier) {
Description description = getDescription();
notifier.fireTestStarted(description);
String targetPath = System.getProperty(PATH_PARAM);
if (targetPath == null) {
for (Description testDescription : description.getChildren()) {
notifier.fireTestIgnored(testDescription);
}
notifier.fireTestIgnored(description);
notifier.fireTestFinished(description);
return;
}
TeaVMTestTool tool = new TeaVMTestTool();
tool.setTargetDirectory(new File(targetPath, testClass.getName()));
tool.setMinifying(false);
tool.getTestClasses().add(testClass.getName());
boolean success = true;
private boolean runInTeaVM(Method child, RunNotifier notifier) {
CompileResult compileResult;
try {
tool.generate();
} catch (Exception e) {
notifier.fireTestFailure(new Failure(description, e));
success = false;
compileResult = compileTest(child);
} catch (IOException e) {
notifier.fireTestFailure(new Failure(describeChild(child), e));
return false;
}
if (success) {
for (Description testDescription : description.getChildren()) {
notifier.fireTestStarted(testDescription);
notifier.fireTestFinished(testDescription);
if (!compileResult.success) {
notifier.fireTestFailure(new Failure(describeChild(child),
new AssertionError(compileResult.errorMessage)));
return false;
}
return true;
}
private CompileResult compileTest(Method method) throws IOException {
CompileResult result = new CompileResult();
File path = outputDir;
path = new File(path, method.getDeclaringClass().getName().replace('.', '/'));
path = new File(path, method.getName());
path.mkdirs();
File outputFile = new File(path, "test.js");
result.file = outputFile;
resourceToFile("org/teavm/javascript/runtime.js", new File(path, "runtime.js"));
resourceToFile("teavm-run-test.html", new File(path, "run-test.html"));
ClassLoader classLoader = TeaVMTestRunner.class.getClassLoader();
ClassHolderSource classSource = getClassSource(classLoader);
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
Class<?> runnerType = testAdapter.getRunner(methodHolder);
TeaVM vm = new TeaVMBuilder()
.setClassLoader(classLoader)
.setClassSource(classSource)
.build();
vm.setIncremental(false);
vm.setMinifying(false);
vm.installPlugins();
new TestExceptionPlugin().install(vm);
new TestEntryPointTransformer(runnerType.getName(), methodHolder.getReference()).install(vm);
try (Writer innerWriter = new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8")) {
MethodReference exceptionMsg = new MethodReference(ExceptionHelper.class, "showException",
Throwable.class, String.class);
vm.entryPoint("runTest", new MethodReference(TestEntryPoint.class, "run", void.class)).async();
vm.entryPoint("extractException", exceptionMsg);
vm.build(innerWriter, new DirectoryBuildTarget(outputDir));
if (!vm.getProblemProvider().getProblems().isEmpty()) {
result.success = false;
result.errorMessage = buildErrorMessage(vm);
}
}
notifier.fireTestFinished(description);
return result;
}
private MethodDescriptor getDescriptor(Method method) {
ValueType[] signature = Stream.concat(Arrays.stream(method.getParameterTypes()).map(ValueType::parse),
Stream.of(ValueType.parse(method.getReturnType())))
.toArray(ValueType[]::new);
return new MethodDescriptor(method.getName(), signature);
}
private String buildErrorMessage(TeaVM vm) {
CallGraph cg = vm.getDependencyInfo().getCallGraph();
DefaultProblemTextConsumer consumer = new DefaultProblemTextConsumer();
StringBuilder sb = new StringBuilder();
for (Problem problem : vm.getProblemProvider().getProblems()) {
consumer.clear();
problem.render(consumer);
sb.append(consumer.getText());
TeaVMProblemRenderer.renderCallStack(cg, problem.getLocation(), sb);
sb.append("\n");
}
return sb.toString();
}
private void resourceToFile(String resource, File fileName) throws IOException {
try (InputStream input = TeaVMTestTool.class.getClassLoader().getResourceAsStream(resource);
OutputStream output = new FileOutputStream(fileName)) {
IOUtils.copy(input, output);
}
}
private static ClassHolderSource getClassSource(ClassLoader classLoader) {
return classSources.computeIfAbsent(classLoader, cl -> new PreOptimizingClassHolderSource(
new ClasspathClassHolderSource(classLoader)));
}
static class CompileResult {
boolean success = true;
String errorMessage;
File file;
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import org.teavm.testing.TestRunner;
final class TestEntryPoint {
private static Object testCase;
private TestEntryPoint() {
}
public static void run() throws Throwable {
createRunner().run(() -> launchTest());
}
private static native TestRunner createRunner();
private static native void launchTest();
private static native boolean isExpectedException(Class<?> cls);
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import org.teavm.diagnostics.Diagnostics;
import org.teavm.model.ClassHolder;
import org.teavm.model.ClassHolderTransformer;
import org.teavm.model.ClassReaderSource;
import org.teavm.model.ElementModifier;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.ValueType;
import org.teavm.model.emit.ProgramEmitter;
import org.teavm.model.emit.ValueEmitter;
import org.teavm.vm.spi.TeaVMHost;
import org.teavm.vm.spi.TeaVMPlugin;
class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
private String runnerClassName;
private MethodReference testMethod;
public TestEntryPointTransformer(String runnerClassName, MethodReference testMethod) {
this.runnerClassName = runnerClassName;
this.testMethod = testMethod;
}
@Override
public void install(TeaVMHost host) {
host.add(this);
}
@Override
public void transformClass(ClassHolder cls, ClassReaderSource innerSource, Diagnostics diagnostics) {
if (cls.getName().equals(TestEntryPoint.class.getName())) {
for (MethodHolder method : cls.getMethods()) {
if (method.getName().equals("createRunner")) {
method.setProgram(generateRunnerProgram(method, innerSource));
method.getModifiers().remove(ElementModifier.NATIVE);
} else if (method.getName().equals("launchTest")) {
method.setProgram(generateLaunchProgram(method, innerSource));
method.getModifiers().remove(ElementModifier.NATIVE);
}
}
}
}
private Program generateRunnerProgram(MethodHolder method, ClassReaderSource innerSource) {
ProgramEmitter pe = ProgramEmitter.create(method, innerSource);
pe.construct(runnerClassName).returnValue();
return pe.getProgram();
}
private Program generateLaunchProgram(MethodHolder method, ClassReaderSource innerSource) {
ProgramEmitter pe = ProgramEmitter.create(method, innerSource);
ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class);
pe.when(testCaseVar.isNull())
.thenDo(() -> {
pe.setField(TestEntryPoint.class, "testCase",
pe.construct(testMethod.getClassName()).cast(Object.class));
});
pe.getField(TestEntryPoint.class, "testCase", Object.class)
.cast(ValueType.object(testMethod.getClassName()))
.invokeSpecial(testMethod);
pe.exit();
return pe.getProgram();
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import org.teavm.dependency.AbstractDependencyListener;
import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.DependencyNode;
import org.teavm.dependency.MethodDependency;
import org.teavm.model.CallLocation;
import org.teavm.model.ClassReader;
import org.teavm.model.ClassReaderSource;
import org.teavm.model.MethodReference;
class TestExceptionDependency extends AbstractDependencyListener {
private MethodReference getMessageRef = new MethodReference(ExceptionHelper.class, "showException",
Throwable.class, String.class);
private DependencyNode allClasses;
@Override
public void started(DependencyAgent agent) {
allClasses = agent.createNode();
}
@Override
public void classReached(DependencyAgent agent, String className, CallLocation location) {
if (isException(agent.getClassSource(), className)) {
allClasses.propagate(agent.getType(className));
}
}
private boolean isException(ClassReaderSource classSource, String className) {
while (className != null) {
if (className.equals("java.lang.Throwable")) {
return true;
}
ClassReader cls = classSource.get(className);
if (cls == null) {
return false;
}
className = cls.getParent();
}
return false;
}
@Override
public void methodReached(DependencyAgent agent, MethodDependency method, CallLocation location) {
if (method.getReference().equals(getMessageRef)) {
allClasses.connect(method.getVariable(1));
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2016 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.junit;
import org.teavm.vm.spi.TeaVMHost;
import org.teavm.vm.spi.TeaVMPlugin;
class TestExceptionPlugin implements TeaVMPlugin {
@Override
public void install(TeaVMHost host) {
host.add(new TestExceptionDependency());
}
}

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<title>TeaVM JUnit test</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="test.js"></script>
<script type="text/javascript">
$rt_startThread(function() {
var thread = $rt_nativeThread();
var instance;
var ptr = 0;
var message;
if (thread.isResuming()) {
ptr = thread.pop();
instance = thread.pop();
}
loop: while (true) {
switch (ptr) {
case 0:
try {
runTest();
} catch (e) {
message = buildErrorMessage(e);
break loop;
}
if (thread.isSuspending()) {
thread.push(instance);
thread.push(ptr);
return;
}
message = "OK";
break loop;
}
}
document.body.appendChild(document.createTextNode(message));
});
function buildErrorMessage(e) {
var stack = e.stack;
if (e.$javaException && e.$javaException.constructor.$meta) {
stack = e.$javaException.constructor.$meta.name + ": ";
var exceptionMessage = extractException(e.$javaException);
stack += exceptionMessage ? $rt_ustr(exceptionMessage) : "";
}
stack += "\n" + stack;
return stack;
}
</script>
</body>
</html>