Improving JUnit test runner

This commit is contained in:
Alexey Andreev 2016-02-26 23:48:04 +03:00
parent c808f41a8f
commit 5576275998
17 changed files with 192 additions and 333 deletions

View File

@ -13,7 +13,7 @@
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" /> <option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="-ea -Dteavm.junit.target=target/js-tests" /> <option name="VM_PARAMETERS" value="-ea -Dteavm.junit.target=target/js-tests -Dteavm.junit.js.runner=htmlunit -Dteavm.junit.js.threads=2" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$" /> <option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$" />
<option name="ENV_VARIABLES" /> <option name="ENV_VARIABLES" />

View File

@ -21,6 +21,7 @@ import org.teavm.classlib.impl.tz.DateTimeZone;
import org.teavm.classlib.impl.tz.DateTimeZoneProvider; import org.teavm.classlib.impl.tz.DateTimeZoneProvider;
import org.teavm.classlib.impl.tz.FixedDateTimeZone; import org.teavm.classlib.impl.tz.FixedDateTimeZone;
import org.teavm.classlib.impl.unicode.CLDRHelper; import org.teavm.classlib.impl.unicode.CLDRHelper;
import org.teavm.classlib.java.lang.TThreadInterruptHandler;
/** /**
* {@code TimeZone} represents a time zone offset, taking into account * {@code TimeZone} represents a time zone offset, taking into account
@ -152,7 +153,8 @@ public abstract class TTimeZone implements Serializable, Cloneable {
*/ */
public static TTimeZone getDefault() { public static TTimeZone getDefault() {
if (defaultTz == null) { if (defaultTz == null) {
defaultTz = new TIANATimeZone(DateTimeZoneProvider.detectTimezone()); //defaultTz = new TIANATimeZone(DateTimeZoneProvider.detectTimezone());
defaultTz = TTimeZone.getTimeZone("UTC");
} }
return (TTimeZone) defaultTz.clone(); return (TTimeZone) defaultTz.clone();
} }

View File

@ -15,14 +15,6 @@
*/ */
package org.teavm.classlib.java.util; package org.teavm.classlib.java.util;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMTestRunner;
/**
*
* @author Alexey Andreev
*/
@RunWith(TeaVMTestRunner.class)
public interface TestService { public interface TestService {
void foo(); void foo();
} }

View File

@ -52,24 +52,24 @@
<orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.6.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.6.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.6.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.6.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-core:2.6.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-core:2.6.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.htmlunit:htmlunit:2.18" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.htmlunit:htmlunit:2.19" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: xalan:xalan:2.7.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: xalan:xalan:2.7.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: xalan:serializer:2.7.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: xalan:serializer:2.7.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: commons-collections:commons-collections:3.2.1" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: commons-collections:commons-collections:3.2.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.apache.commons:commons-lang3:3.4" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.apache.commons:commons-lang3:3.4" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.apache.httpcomponents:httpmime:4.5" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.apache.httpcomponents:httpmime:4.5.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: commons-codec:commons-codec:1.10" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: commons-codec:commons-codec:1.10" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.htmlunit:htmlunit-core-js:2.17" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.htmlunit:htmlunit-core-js:2.17" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: xerces:xercesImpl:2.11.0" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: xerces:xercesImpl:2.11.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: xml-apis:xml-apis:1.4.01" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: xml-apis:xml-apis:1.4.01" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.nekohtml:nekohtml:1.9.22" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.nekohtml:nekohtml:1.9.22" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.cssparser:cssparser:0.9.16" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: net.sourceforge.cssparser:cssparser:0.9.18" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.w3c.css:sac:1.3" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.w3c.css:sac:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: commons-logging:commons-logging:1.2" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: commons-logging:commons-logging:1.2" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.2.12.v20150709" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.2.13.v20150730" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty:jetty-util:9.2.12.v20150709" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty:jetty-util:9.2.13.v20150730" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty:jetty-io:9.2.12.v20150709" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty:jetty-io:9.2.13.v20150730" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.2.12.v20150709" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.2.13.v20150730" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.2.12.v20150709" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.2.13.v20150730" level="project" />
</component> </component>
</module> </module>

View File

@ -1,215 +0,0 @@
/*
* Copyright 2014 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.cli;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.teavm.model.ClassHolderTransformer;
import org.teavm.testing.TestAdapter;
import org.teavm.tooling.TeaVMToolException;
import org.teavm.tooling.testing.TeaVMTestTool;
/**
*
* @author Alexey Andreev
*/
public final class TeaVMTestRunner {
private TeaVMTestRunner() {
}
@SuppressWarnings("static-access")
public static void main(String[] args) {
Options options = new Options();
options.addOption(OptionBuilder
.withArgName("directory")
.hasArg()
.withDescription("a directory where to put generated files (current directory by default)")
.withLongOpt("targetdir")
.create('d'));
options.addOption(OptionBuilder
.withDescription("causes TeaVM to generate minimized JavaScript file")
.withLongOpt("minify")
.create("m"));
options.addOption(OptionBuilder
.withArgName("number")
.hasArg()
.withDescription("how many threads should TeaVM run")
.withLongOpt("threads")
.create("t"));
options.addOption(OptionBuilder
.withArgName("class name")
.hasArg()
.withDescription("qualified class name of test adapter")
.withLongOpt("adapter")
.create("a"));
options.addOption(OptionBuilder
.hasArg()
.hasOptionalArgs()
.withArgName("class name")
.withDescription("qualified class names of transformers")
.withLongOpt("transformers")
.create("T"));
options.addOption(OptionBuilder
.withDescription("Incremental build")
.withLongOpt("incremental")
.create('i'));
if (args.length == 0) {
printUsage(options);
return;
}
CommandLineParser parser = new PosixParser();
CommandLine commandLine;
try {
commandLine = parser.parse(options, args);
} catch (ParseException e) {
printUsage(options);
return;
}
TeaVMTestTool tool = new TeaVMTestTool();
tool.setTargetDirectory(new File(commandLine.getOptionValue("d", ".")));
tool.setMinifying(commandLine.hasOption("m"));
try {
tool.setNumThreads(Integer.parseInt(commandLine.getOptionValue("t", "1")));
} catch (NumberFormatException e) {
System.err.println("Invalid number specified for -t option");
printUsage(options);
return;
}
if (commandLine.hasOption("a")) {
tool.setAdapter(instantiateAdapter(commandLine.getOptionValue("a")));
}
if (commandLine.hasOption("T")) {
for (String transformerType : commandLine.getOptionValues("T")) {
tool.getTransformers().add(instantiateTransformer(transformerType));
}
}
if (commandLine.hasOption('i')) {
tool.setIncremental(true);
}
args = commandLine.getArgs();
if (args.length == 0) {
System.err.println("You did not specify any test classes");
printUsage(options);
return;
}
tool.getTestClasses().addAll(Arrays.asList(args));
tool.setLog(new ConsoleTeaVMToolLog());
tool.getProperties().putAll(System.getProperties());
long start = System.currentTimeMillis();
try {
tool.generate();
} catch (TeaVMToolException e) {
e.printStackTrace(System.err);
System.exit(-2);
}
System.out.println("Operation took " + (System.currentTimeMillis() - start) + " milliseconds");
}
private static TestAdapter instantiateAdapter(String adapterName) {
Class<?> adapterClass;
try {
adapterClass = Class.forName(adapterName, true, TeaVMTestRunner.class.getClassLoader());
} catch (ClassNotFoundException e) {
System.err.println("Adapter not found: " + adapterName);
System.exit(-1);
return null;
}
if (!TestAdapter.class.isAssignableFrom(adapterClass)) {
System.err.println("Adapter class does not implement TestAdapter: " + adapterName);
System.exit(-1);
return null;
}
Constructor<?> cons;
try {
cons = adapterClass.getConstructor();
} catch (NoSuchMethodException e) {
System.err.println("Adapter class does not contain no-arg constructor: " + adapterName);
System.exit(-1);
return null;
}
try {
return (TestAdapter) cons.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
System.err.println("Error instantiating adapter: " + adapterName);
e.printStackTrace(System.err);
System.exit(-1);
return null;
} catch (InvocationTargetException e) {
System.err.println("Error instantiating adapter: " + adapterName);
e.getTargetException().printStackTrace(System.err);
System.exit(-1);
return null;
}
}
private static ClassHolderTransformer instantiateTransformer(String transformerName) {
Class<?> adapterClass;
try {
adapterClass = Class.forName(transformerName, true, TeaVMTestRunner.class.getClassLoader());
} catch (ClassNotFoundException e) {
System.err.println("Transformer not found: " + transformerName);
System.exit(-1);
return null;
}
if (!ClassHolderTransformer.class.isAssignableFrom(adapterClass)) {
System.err.println("Transformer class does not implement ClassHolderTransformer: " + transformerName);
System.exit(-1);
return null;
}
Constructor<?> cons;
try {
cons = adapterClass.getConstructor();
} catch (NoSuchMethodException e) {
System.err.println("Transformer class does not contain no-arg constructor: " + transformerName);
System.exit(-1);
return null;
}
try {
return (ClassHolderTransformer) cons.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
System.err.println("Error instantiating transformer: " + transformerName);
e.printStackTrace(System.err);
System.exit(-1);
return null;
} catch (InvocationTargetException e) {
System.err.println("Error instantiating transformer: " + transformerName);
e.getTargetException().printStackTrace(System.err);
System.exit(-1);
return null;
}
}
private static void printUsage(Options options) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("java " + TeaVMTestRunner.class.getName() + " [OPTIONS] test_name {test_name}", options);
System.exit(-1);
}
}

View File

@ -53,7 +53,7 @@
<dependency> <dependency>
<groupId>net.sourceforge.htmlunit</groupId> <groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId> <artifactId>htmlunit</artifactId>
<version>2.18</version> <version>2.19</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -15,10 +15,6 @@
*/ */
package org.teavm.junit; package org.teavm.junit;
/**
*
* @author Alexey Andreev
*/
final class ExceptionHelper { final class ExceptionHelper {
private ExceptionHelper() { private ExceptionHelper() {
} }

View File

@ -29,33 +29,56 @@ import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.NativeJavaObject; import net.sourceforge.htmlunit.corejs.javascript.NativeJavaObject;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
public class HtmlUnitRunStrategy implements TestRunStrategy { class HtmlUnitRunStrategy implements TestRunStrategy {
private ThreadLocal<WebClient> webClient = new ThreadLocal<>();
private ThreadLocal<HtmlPage> page = new ThreadLocal<>();
private int runs;
@Override @Override
public void beforeThread() { public void beforeThread() {
init();
} }
@Override @Override
public void afterThread() { public void afterThread() {
cleanUp();
} }
@Override @Override
public String runTest(TestRun run) throws IOException { public String runTest(TestRun run) throws IOException {
try (WebClient webClient = new WebClient(BrowserVersion.CHROME)) { if (++runs == 50) {
HtmlPage page = webClient.getPage("about:blank"); runs = 0;
page.executeJavaScript(readFile(new File(run.getBaseDirectory(), "runtime.js"))); cleanUp();
init();
}
AsyncResult asyncResult = new AsyncResult(); page.get().executeJavaScript(readFile(new File(run.getBaseDirectory(), "runtime.js")));
Function function = (Function) page.executeJavaScript(readResource("teavm-htmlunit-adapter.js")) page.get().executeJavaScript(readFile(new File(run.getBaseDirectory(), "test.js")));
.getJavaScriptResult();
Object[] args = new Object[] { new NativeJavaObject(function, asyncResult, AsyncResult.class) };
page.executeJavaScriptFunctionIfPossible(function, function, args, page);
page.executeJavaScript(readFile(new File(run.getBaseDirectory(), "test.js"))); AsyncResult asyncResult = new AsyncResult();
page.cleanUp(); Function function = (Function) page.get().executeJavaScript(readResource("teavm-htmlunit-adapter.js"))
for (WebWindow window : webClient.getWebWindows()) { .getJavaScriptResult();
window.getJobManager().removeAllJobs(); Object[] args = new Object[] { new NativeJavaObject(function, asyncResult, AsyncResult.class) };
} page.get().executeJavaScriptFunctionIfPossible(function, function, args, page.get());
return (String) asyncResult.getResult(); return (String) asyncResult.getResult();
}
private void cleanUp() {
page.get().cleanUp();
for (WebWindow window : webClient.get().getWebWindows()) {
window.getJobManager().removeAllJobs();
}
page.remove();
webClient.get().close();
webClient.remove();
}
private void init() {
webClient.set(new WebClient(BrowserVersion.CHROME));
try {
page.set(webClient.get().getPage("about:blank"));
} catch (IOException e) {
throw new RuntimeException(e);
} }
} }

View File

@ -28,10 +28,11 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.RemoteWebDriver;
public class SeleniumRunStrategy implements TestRunStrategy { class SeleniumRunStrategy implements TestRunStrategy {
private URL url; private URL url;
private ThreadLocal<WebDriver> webDriver = new ThreadLocal<>(); private ThreadLocal<WebDriver> webDriver = new ThreadLocal<>();
private ThreadLocal<Integer> commandsSent = new ThreadLocal<>(); private ThreadLocal<Integer> commandsSent = new ThreadLocal<>();
private int rebootRate = Integer.getInteger("teavm.junit.js.selenium.rebootAfter", 1000);
public SeleniumRunStrategy(URL url) { public SeleniumRunStrategy(URL url) {
this.url = url; this.url = url;
@ -54,7 +55,7 @@ public class SeleniumRunStrategy implements TestRunStrategy {
@Override @Override
public String runTest(TestRun run) throws IOException { public String runTest(TestRun run) throws IOException {
commandsSent.set(commandsSent.get() + 1); commandsSent.set(commandsSent.get() + 1);
if (commandsSent.get().equals(20)) { if (commandsSent.get().equals(100)) {
commandsSent.set(0); commandsSent.set(0);
webDriver.get().close(); webDriver.get().close();
webDriver.get().quit(); webDriver.get().quit();

View File

@ -66,6 +66,7 @@ import org.teavm.vm.TeaVMBuilder;
public class TeaVMTestRunner extends Runner { public class TeaVMTestRunner extends Runner {
private static final String PATH_PARAM = "teavm.junit.target"; private static final String PATH_PARAM = "teavm.junit.target";
private static final String RUNNER = "teavm.junit.js.runner"; private static final String RUNNER = "teavm.junit.js.runner";
private static final String THREAD_COUNT = "teavm.junit.js.threads";
private static final String SELENIUM_URL = "teavm.junit.js.selenium.url"; private static final String SELENIUM_URL = "teavm.junit.js.selenium.url";
private static final int stopTimeout = 15000; private static final int stopTimeout = 15000;
private Class<?> testClass; private Class<?> testClass;
@ -83,6 +84,16 @@ public class TeaVMTestRunner extends Runner {
private static volatile ScheduledFuture<?> cleanupFuture; private static volatile ScheduledFuture<?> cleanupFuture;
private CountDownLatch latch; private CountDownLatch latch;
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
synchronized (TeaVMTestRunner.class) {
cleanupFuture = null;
runner.stop();
runner.waitForCompletion();
}
}));
}
public TeaVMTestRunner(Class<?> testClass) throws InitializationError { public TeaVMTestRunner(Class<?> testClass) throws InitializationError {
this.testClass = testClass; this.testClass = testClass;
classLoader = TeaVMTestRunner.class.getClassLoader(); classLoader = TeaVMTestRunner.class.getClassLoader();
@ -167,14 +178,27 @@ public class TeaVMTestRunner extends Runner {
boolean run = false; boolean run = false;
boolean success = true; boolean success = true;
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));
notifier.fireTestFinished(describeChild(child));
latch.countDown();
return;
}
}
if (!child.isAnnotationPresent(SkipJVM.class) if (!child.isAnnotationPresent(SkipJVM.class)
&& !child.getDeclaringClass().isAnnotationPresent(SkipJVM.class)) { && !child.getDeclaringClass().isAnnotationPresent(SkipJVM.class)) {
run = true; run = true;
success = runInJvm(child, notifier); success = runInJvm(child, notifier, expectedExceptions);
} }
if (success && outputDir != null) { if (success && outputDir != null) {
runInTeaVM(child, notifier); runInTeaVM(child, notifier, expectedExceptions);
} else { } else {
if (!run) { if (!run) {
notifier.fireTestIgnored(describeChild(child)); notifier.fireTestIgnored(describeChild(child));
@ -184,18 +208,7 @@ public class TeaVMTestRunner extends Runner {
} }
} }
private boolean runInJvm(Method child, RunNotifier notifier) { private boolean runInJvm(Method child, RunNotifier notifier, Set<Class<?>> expectedExceptions) {
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; Object instance;
try { try {
instance = testClass.newInstance(); instance = testClass.newInstance();
@ -233,22 +246,29 @@ public class TeaVMTestRunner extends Runner {
return true; return true;
} }
private boolean runInTeaVM(Method child, RunNotifier notifier) { private boolean runInTeaVM(Method child, RunNotifier notifier, Set<Class<?>> expectedExceptions) {
Description description = describeChild(child);
CompileResult compileResult; CompileResult compileResult;
try { try {
compileResult = compileTest(child); compileResult = compileTest(child);
} catch (IOException e) { } catch (IOException e) {
notifier.fireTestFailure(new Failure(describeChild(child), e)); notifier.fireTestFailure(new Failure(description, e));
return false; return false;
} }
if (!compileResult.success) { if (!compileResult.success) {
notifier.fireTestFailure(new Failure(describeChild(child), notifier.fireTestFailure(new Failure(description,
new AssertionError(compileResult.errorMessage))); new AssertionError(compileResult.errorMessage)));
return false; return false;
} }
Description description = describeChild(child); if (runStrategy == null) {
notifier.fireTestFinished(description);
latch.countDown();
return true;
}
TestRunCallback callback = new TestRunCallback() { TestRunCallback callback = new TestRunCallback() {
@Override @Override
public void complete() { public void complete() {
@ -264,7 +284,7 @@ public class TeaVMTestRunner extends Runner {
TestRun run = new TestRun(compileResult.file.getParentFile(), child, TestRun run = new TestRun(compileResult.file.getParentFile(), child,
new MethodReference(testClass.getName(), getDescriptor(child)), new MethodReference(testClass.getName(), getDescriptor(child)),
description, callback); description, callback, expectedExceptions);
submitRun(run); submitRun(run);
return true; return true;
} }
@ -277,6 +297,11 @@ public class TeaVMTestRunner extends Runner {
if (runner == null) { if (runner == null) {
runner = new TestRunner(runStrategy); runner = new TestRunner(runStrategy);
try {
runner.setNumThreads(Integer.parseInt(System.getProperty(THREAD_COUNT, "1")));
} catch (NumberFormatException e) {
runner.setNumThreads(1);
}
runner.init(); runner.init();
} }
runner.run(run); runner.run(run);

View File

@ -17,23 +17,28 @@ package org.teavm.junit;
import java.io.File; import java.io.File;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
public class TestRun { class TestRun {
private File baseDirectory; private File baseDirectory;
private Method method; private Method method;
private MethodReference reference; private MethodReference reference;
private Description description; private Description description;
private TestRunCallback callback; private TestRunCallback callback;
private Set<Class<?>> expectedExceptions;
public TestRun(File baseDirectory, Method method, MethodReference reference, Description description, TestRun(File baseDirectory, Method method, MethodReference reference, Description description,
TestRunCallback callback) { TestRunCallback callback, Set<Class<?>> expectedExceptions) {
this.baseDirectory = baseDirectory; this.baseDirectory = baseDirectory;
this.method = method; this.method = method;
this.reference = reference; this.reference = reference;
this.description = description; this.description = description;
this.callback = callback; this.callback = callback;
this.expectedExceptions = Collections.unmodifiableSet(new HashSet<>(expectedExceptions));
} }
public File getBaseDirectory() { public File getBaseDirectory() {
@ -55,4 +60,8 @@ public class TestRun {
public TestRunCallback getCallback() { public TestRunCallback getCallback() {
return callback; return callback;
} }
public Set<Class<?>> getExpectedExceptions() {
return expectedExceptions;
}
} }

View File

@ -15,7 +15,7 @@
*/ */
package org.teavm.junit; package org.teavm.junit;
public interface TestRunCallback { interface TestRunCallback {
void complete(); void complete();
void error(Throwable e); void error(Throwable e);

View File

@ -17,7 +17,7 @@ package org.teavm.junit;
import java.io.IOException; import java.io.IOException;
public interface TestRunStrategy { interface TestRunStrategy {
void beforeThread(); void beforeThread();
void afterThread(); void afterThread();

View File

@ -22,18 +22,15 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.teavm.model.MethodReference;
public class TestRunner { class TestRunner {
private int numThreads = 1; private int numThreads = 1;
private TestRunStrategy strategy; private TestRunStrategy strategy;
private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(); private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
private CountDownLatch latch; private CountDownLatch latch;
private volatile boolean stopped; private volatile boolean stopped;
public TestRunner(TestRunStrategy strategy) { TestRunner(TestRunStrategy strategy) {
this.strategy = strategy; this.strategy = strategy;
} }
@ -73,7 +70,19 @@ public class TestRunner {
public void stop() { public void stop() {
stopped = true; stopped = true;
taskQueue.add(null); taskQueue.add(() -> { });
}
public void waitForCompletion() {
while (true) {
try {
if (latch.await(1000, TimeUnit.MILLISECONDS)) {
break;
}
} catch (InterruptedException e) {
break;
}
}
} }
public void run(TestRun run) { public void run(TestRun run) {
@ -81,7 +90,6 @@ public class TestRunner {
} }
private void runImpl(TestRun run) { private void runImpl(TestRun run) {
MethodReference ref = run.getReference();
try { try {
String result = strategy.runTest(run); String result = strategy.runTest(run);
if (result == null) { if (result == null) {
@ -93,17 +101,38 @@ public class TestRunner {
String status = resultObject.get("status").asText(); String status = resultObject.get("status").asText();
switch (status) { switch (status) {
case "ok": case "ok":
run.getCallback().complete(); if (!run.getExpectedExceptions().isEmpty()) {
run.getCallback().error(new AssertionError("Expected exception was not thrown"));
} else {
run.getCallback().complete();
}
break; break;
case "exception": { case "exception": {
String stack = resultObject.get("stack").asText(); String stack = resultObject.get("stack").asText();
String exception = resultObject.get("exception").asText(); String exception = resultObject.has("exception") ? resultObject.get("exception").asText() : null;
run.getCallback().error(new AssertionError(exception + "\n" + exception)); Class<?> exceptionClass;
if (exception != null) {
try {
exceptionClass = Class.forName(exception, false, TestRunner.class.getClassLoader());
} catch (ClassNotFoundException e) {
exceptionClass = null;
}
} else {
exceptionClass = null;
}
if (exceptionClass != null) {
Class<?> caught = exceptionClass;
if (run.getExpectedExceptions().stream().anyMatch(e -> e.isAssignableFrom(caught))) {
run.getCallback().complete();
break;
}
}
run.getCallback().error(new AssertionError(exception + "\n" + stack));
run.getCallback().complete(); run.getCallback().complete();
break; break;
} }
} }
} catch (IOException e) { } catch (Exception e) {
run.getCallback().error(e); run.getCallback().error(e);
run.getCallback().complete(); run.getCallback().complete();
} }

View File

@ -1,38 +1,37 @@
function main(callback) { function main(callback) {
var JUnitClient = {}; $rt_startThread(function () {
JUnitClient.run = function() { var thread = $rt_nativeThread();
$rt_startThread(function() { var instance;
var thread = $rt_nativeThread(); var ptr = 0;
var instance; var message;
var ptr = 0; if (thread.isResuming()) {
var message; ptr = thread.pop();
if (thread.isResuming()) { instance = thread.pop();
ptr = thread.pop(); }
instance = thread.pop(); loop: while (true) {
} switch (ptr) {
loop: while (true) { switch (ptr) { case 0:
case 0: try {
try { runTest();
runTest(); } catch (e) {
} catch (e) { message = {};
makeErrorMessage(message, e);
break loop;
}
if (thread.isSuspending()) {
thread.push(instance);
thread.push(ptr);
return;
}
message = {}; message = {};
JUnitClient.makeErrorMessage(message, e); message.status = "ok";
break loop; break loop;
} }
if (thread.isSuspending()) { }
thread.push(instance); callback.complete(JSON.stringify(message));
thread.push(ptr); });
return;
}
message = {};
message.status = "ok";
break loop;
}}
callback.complete(JSON.stringify(message));
})
};
JUnitClient.makeErrorMessage = function(message, e) { function makeErrorMessage(message, e) {
message.status = "exception"; message.status = "exception";
var stack = e.stack; var stack = e.stack;
if (e.$javaException && e.$javaException.constructor.$meta) { if (e.$javaException && e.$javaException.constructor.$meta) {
@ -42,7 +41,5 @@ function main(callback) {
message.stack += exceptionMessage ? $rt_ustr(exceptionMessage) : ""; message.stack += exceptionMessage ? $rt_ustr(exceptionMessage) : "";
} }
message.stack += "\n" + stack; message.stack += "\n" + stack;
}; }
window.JUnitClient = JUnitClient;
} }

View File

@ -29,7 +29,7 @@ $rt_startThread(function() {
runTest(); runTest();
} catch (e) { } catch (e) {
message = {}; message = {};
JUnitClient.makeErrorMessage(message, e); makeErrorMessage(message, e);
break loop; break loop;
} }
if (thread.isSuspending()) { if (thread.isSuspending()) {

View File

@ -41,25 +41,25 @@
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.6.2" level="project" /> <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.6.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.6.2" level="project" /> <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.6.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.6.2" level="project" /> <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.6.2" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit:2.18" level="project" /> <orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit:2.19" level="project" />
<orderEntry type="library" name="Maven: xalan:xalan:2.7.2" level="project" /> <orderEntry type="library" name="Maven: xalan:xalan:2.7.2" level="project" />
<orderEntry type="library" name="Maven: xalan:serializer:2.7.2" level="project" /> <orderEntry type="library" name="Maven: xalan:serializer:2.7.2" level="project" />
<orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.1" level="project" /> <orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.4" level="project" /> <orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.4" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpmime:4.5" level="project" /> <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpmime:4.5.1" level="project" />
<orderEntry type="library" name="Maven: commons-codec:commons-codec:1.10" level="project" /> <orderEntry type="library" name="Maven: commons-codec:commons-codec:1.10" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit-core-js:2.17" level="project" /> <orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit-core-js:2.17" level="project" />
<orderEntry type="library" name="Maven: xerces:xercesImpl:2.11.0" level="project" /> <orderEntry type="library" name="Maven: xerces:xercesImpl:2.11.0" level="project" />
<orderEntry type="library" name="Maven: xml-apis:xml-apis:1.4.01" level="project" /> <orderEntry type="library" name="Maven: xml-apis:xml-apis:1.4.01" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.nekohtml:nekohtml:1.9.22" level="project" /> <orderEntry type="library" name="Maven: net.sourceforge.nekohtml:nekohtml:1.9.22" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.cssparser:cssparser:0.9.16" level="project" /> <orderEntry type="library" name="Maven: net.sourceforge.cssparser:cssparser:0.9.18" level="project" />
<orderEntry type="library" name="Maven: org.w3c.css:sac:1.3" level="project" /> <orderEntry type="library" name="Maven: org.w3c.css:sac:1.3" level="project" />
<orderEntry type="library" name="Maven: commons-io:commons-io:2.4" level="project" /> <orderEntry type="library" name="Maven: commons-io:commons-io:2.4" level="project" />
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.2.12.v20150709" level="project" /> <orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.2.13.v20150730" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-util:9.2.12.v20150709" level="project" /> <orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-util:9.2.13.v20150730" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-io:9.2.12.v20150709" level="project" /> <orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-io:9.2.13.v20150730" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.2.12.v20150709" level="project" /> <orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.2.13.v20150730" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.2.12.v20150709" level="project" /> <orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.2.13.v20150730" level="project" />
</component> </component>
</module> </module>