Add HtmlUnit test runner. Add travis + Sauce Labs config

This commit is contained in:
Alexey Andreev 2015-10-10 11:06:30 +03:00
parent 010a5fe579
commit bac0785dc6
13 changed files with 397 additions and 89 deletions

View File

@ -6,3 +6,8 @@ cache:
- $HOME/.m2
after_script:
- rm -rf $HOME/.m2/repository/org/teavm
script:
- mvn test \
- -Pteavm.test.skip=false \
- -Pteavm.test.selenium="http://$SAUCE_USER_NAME:$SAUCE_ACCESS_KEY@ondemand.saucelabs.com:80/wd/hub" \
- -Pteavm.test.threads=2

View File

@ -31,6 +31,7 @@
<teavm.test.incremental>false</teavm.test.incremental>
<teavm.test.threads>1</teavm.test.threads>
<teavm.test.selenium></teavm.test.selenium>
<teavm.test.skip>true</teavm.test.skip>
</properties>
<dependencies>
@ -100,6 +101,7 @@
<goal>test</goal>
</goals>
<configuration>
<skip>${teavm.test.skip}</skip>
<numThreads>${teavm.test.threads}</numThreads>
<seleniumURL>${teavm.test.selenium}</seleniumURL>
</configuration>

View File

@ -69,6 +69,11 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.18</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -103,6 +108,14 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<configLocation>../../../checkstyle.xml</configLocation>
<propertyExpansion>config_loc=${basedir}/../../..</propertyExpansion>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -106,8 +106,8 @@ public abstract class AbstractJavascriptMojo extends AbstractMojo {
throw new MojoExecutionException("Transformer not found: " + transformerName, e);
}
if (!ClassHolderTransformer.class.isAssignableFrom(transformerRawType)) {
throw new MojoExecutionException("Transformer " + transformerName + " is not subtype of " +
ClassHolderTransformer.class.getName());
throw new MojoExecutionException("Transformer " + transformerName + " is not subtype of "
+ ClassHolderTransformer.class.getName());
}
Class<? extends ClassHolderTransformer> transformerType = transformerRawType.asSubclass(
ClassHolderTransformer.class);

View File

@ -85,8 +85,8 @@ public class BuildJavascriptTestMojo extends AbstractJavascriptMojo {
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (System.getProperty("maven.test.skip", "false").equals("true") ||
System.getProperty("skipTests") != null) {
if (System.getProperty("maven.test.skip", "false").equals("true")
|| System.getProperty("skipTests") != null) {
getLog().info("Tests build skipped as specified by system property");
return;
}
@ -127,8 +127,8 @@ public class BuildJavascriptTestMojo extends AbstractJavascriptMojo {
throw new MojoExecutionException("Adapter not found: " + adapterClass, e);
}
if (!TestAdapter.class.isAssignableFrom(adapterClsRaw)) {
throw new MojoExecutionException("Adapter " + adapterClass + " does not implement " +
TestAdapter.class.getName());
throw new MojoExecutionException("Adapter " + adapterClass + " does not implement "
+ TestAdapter.class.getName());
}
Class<? extends TestAdapter> adapterCls = adapterClsRaw.asSubclass(TestAdapter.class);
Constructor<? extends TestAdapter> cons;

View File

@ -0,0 +1,107 @@
/*
* Copyright 2015 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.maven;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.NativeJavaObject;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.logging.Log;
import org.teavm.tooling.testing.TestCase;
/**
*
* @author Alexey Andreev
*/
public class HtmlUnitRunStrategy implements TestRunStrategy {
private File directory;
public HtmlUnitRunStrategy(File directory) {
this.directory = directory;
}
@Override
public void beforeThread() {
}
@Override
public void afterThread() {
}
@Override
public String runTest(Log log, String runtimeScript, TestCase testCase) throws IOException {
try (WebClient webClient = new WebClient(BrowserVersion.CHROME)) {
HtmlPage page = webClient.getPage("about:blank");
page.executeJavaScript(readFile(new File(directory, runtimeScript)));
AsyncResult asyncResult = new AsyncResult();
Function function = (Function) page.executeJavaScript(readResource("teavm-htmlunit-adapter.js"))
.getJavaScriptResult();
Object[] args = new Object[] { new NativeJavaObject(function, asyncResult, AsyncResult.class) };
page.executeJavaScriptFunctionIfPossible(function, function, args, page);
page.executeJavaScript(readFile(new File(directory, testCase.getTestScript())));
page.cleanUp();
for (WebWindow window : webClient.getWebWindows()) {
window.getJobManager().removeAllJobs();
}
return (String) asyncResult.getResult();
}
}
private String readFile(File file) throws IOException {
try (InputStream input = new FileInputStream(file)) {
return IOUtils.toString(input, "UTF-8");
}
}
private String readResource(String resourceName) throws IOException {
try (InputStream input = BuildJavascriptTestMojo.class.getClassLoader().getResourceAsStream(resourceName)) {
if (input == null) {
return "";
}
return IOUtils.toString(input, "UTF-8");
}
}
public class AsyncResult {
private CountDownLatch latch = new CountDownLatch(1);
private Object result;
public void complete(Object result) {
this.result = result;
latch.countDown();
}
public Object getResult() {
try {
latch.await(5, TimeUnit.SECONDS);
return result;
} catch (InterruptedException e) {
return null;
}
}
}
}

View File

@ -1,3 +1,18 @@
/*
* Copyright 2015 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.maven;
import org.apache.maven.plugin.logging.Log;

View File

@ -53,28 +53,25 @@ public class RunTestsMojo extends AbstractMojo {
@Parameter
private int numThreads = 1;
@Parameter
private boolean skip;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (seleniumURL == null || seleniumURL.isEmpty()) {
getLog().info("Tests build skipped as selenium URL was not specified");
if (skip) {
getLog().info("Tests run skipped as specified by skip property");
return;
}
if (System.getProperty("maven.test.skip", "false").equals("true") ||
System.getProperty("skipTests") != null) {
getLog().info("Tests build skipped as specified by system property");
if (System.getProperty("maven.test.skip", "false").equals("true")
|| System.getProperty("skipTests") != null) {
getLog().info("Tests run skipped as specified by system property");
return;
}
SeleniumTestRunner runner = new SeleniumTestRunner();
TestRunner runner = new TestRunner(pickStrategy());
runner.setLog(getLog());
runner.setDirectory(testDirectory);
runner.setNumThreads(numThreads);
try {
runner.setUrl(new URL(seleniumURL));
} catch (MalformedURLException e) {
throw new MojoFailureException("Can't parse URL: " + seleniumURL, e);
}
TestPlan plan;
ObjectMapper mapper = new ObjectMapper();
@ -89,6 +86,18 @@ public class RunTestsMojo extends AbstractMojo {
processReport(runner.getReport());
}
private TestRunStrategy pickStrategy() throws MojoFailureException {
if (seleniumURL != null) {
try {
return new SeleniumRunStrategy(new URL(seleniumURL), testDirectory);
} catch (MalformedURLException e) {
throw new MojoFailureException("Can't parse URL: " + seleniumURL, e);
}
} else {
return new HtmlUnitRunStrategy(testDirectory);
}
}
private void processReport(TestReport report) throws MojoExecutionException, MojoFailureException {
if (report.getResults().isEmpty()) {
getLog().info("No tests ran");

View File

@ -0,0 +1,95 @@
/*
* Copyright 2015 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.maven;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.logging.Log;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.teavm.tooling.testing.TestCase;
/**
*
* @author Alexey Andreev
*/
public class SeleniumRunStrategy implements TestRunStrategy {
private URL url;
private File directory;
private ThreadLocal<WebDriver> webDriver = new ThreadLocal<>();
public SeleniumRunStrategy(URL url, File directory) {
this.url = url;
this.directory = directory;
}
@Override
public void beforeThread() {
RemoteWebDriver driver = new RemoteWebDriver(url, DesiredCapabilities.chrome());
webDriver.set(driver);
}
@Override
public void afterThread() {
webDriver.get().close();
webDriver.remove();
}
@Override
public String runTest(Log log, String runtimeScript, TestCase testCase) throws IOException {
webDriver.get().manage().timeouts().setScriptTimeout(2, TimeUnit.SECONDS);
JavascriptExecutor js = (JavascriptExecutor) webDriver.get();
try {
return (String) js.executeAsyncScript(
readResource("teavm-selenium.js"),
readFile(new File(directory, runtimeScript)),
readFile(new File(directory, testCase.getTestScript())),
readResource("teavm-selenium-adapter.js"));
} catch (WebDriverException e) {
log.error("Error occured running test " + testCase.getTestMethod(), e);
@SuppressWarnings("unchecked")
List<Object> errors = (List<Object>) js.executeScript("return window.jsErrors;");
for (Object error : errors) {
log.error(" -- additional error: " + error);
}
return null;
}
}
private String readFile(File file) throws IOException {
try (InputStream input = new FileInputStream(file)) {
return IOUtils.toString(input, "UTF-8");
}
}
private String readResource(String resourceName) throws IOException {
try (InputStream input = BuildJavascriptTestMojo.class.getClassLoader().getResourceAsStream(resourceName)) {
if (input == null) {
return "";
}
return IOUtils.toString(input, "UTF-8");
}
}
}

View File

@ -1,3 +1,18 @@
/*
* Copyright 2015 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.maven;
import java.util.ArrayList;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2015 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.maven;
import java.io.IOException;
import org.apache.maven.plugin.logging.Log;
import org.teavm.tooling.testing.TestCase;
/**
*
* @author Alexey Andreev
*/
public interface TestRunStrategy {
void beforeThread();
void afterThread();
String runTest(Log log, String runtimeScript, TestCase testCase) throws IOException;
}

View File

@ -17,11 +17,7 @@ package org.teavm.maven;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@ -29,13 +25,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.logging.Log;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.teavm.model.MethodReference;
import org.teavm.tooling.testing.TestCase;
import org.teavm.tooling.testing.TestGroup;
@ -45,38 +35,24 @@ import org.teavm.tooling.testing.TestPlan;
*
* @author Alexey Andreev
*/
public class SeleniumTestRunner {
private URL url;
public class TestRunner {
private int numThreads = 1;
private ThreadLocal<WebDriver> webDriver = new ThreadLocal<>();
private BlockingQueue<Runnable> seleniumTaskQueue = new LinkedBlockingQueue<>();
private TestRunStrategy strategy;
private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
private CountDownLatch latch;
private volatile boolean seleniumStopped = false;
private volatile boolean stopped;
private Log log;
private List<TestResult> report = new CopyOnWriteArrayList<>();
private ThreadLocal<List<TestResult>> localReport = new ThreadLocal<>();
private File directory = new File(".");
public URL getUrl() {
return url;
}
public void setUrl(URL url) {
this.url = url;
public TestRunner(TestRunStrategy strategy) {
this.strategy = strategy;
}
public void setLog(Log log) {
this.log = log;
}
public File getDirectory() {
return directory;
}
public void setDirectory(File directory) {
this.directory = directory;
}
public int getNumThreads() {
return numThreads;
}
@ -100,13 +76,12 @@ public class SeleniumTestRunner {
latch = new CountDownLatch(numThreads);
for (int i = 0; i < numThreads; ++i) {
new Thread(() -> {
RemoteWebDriver driver = new RemoteWebDriver(DesiredCapabilities.chrome());
webDriver.set(driver);
strategy.beforeThread();
localReport.set(new ArrayList<>());
while (!seleniumStopped || !seleniumTaskQueue.isEmpty()) {
while (!stopped || !taskQueue.isEmpty()) {
Runnable task;
try {
task = seleniumTaskQueue.poll(100, TimeUnit.MILLISECONDS);
task = taskQueue.poll(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
break;
}
@ -116,19 +91,18 @@ public class SeleniumTestRunner {
}
report.addAll(localReport.get());
localReport.remove();
webDriver.get().close();
webDriver.remove();
strategy.afterThread();
latch.countDown();
}).start();
}
}
private void addSeleniumTask(Runnable runnable) {
seleniumTaskQueue.add(runnable);
private void addTask(Runnable runnable) {
taskQueue.add(runnable);
}
private void stopSelenium() {
seleniumStopped = true;
stopped = true;
}
private void waitForCompletion() {
@ -140,22 +114,20 @@ public class SeleniumTestRunner {
}
private void run(String runtimeScript, TestCase testCase) {
addSeleniumTask(() -> runImpl(runtimeScript, testCase));
addTask(() -> runImpl(runtimeScript, testCase));
}
private void runImpl(String runtimeScript, TestCase testCase) {
webDriver.get().manage().timeouts().setScriptTimeout(2, TimeUnit.SECONDS);
JavascriptExecutor js = (JavascriptExecutor) webDriver.get();
MethodReference ref = MethodReference.parse(testCase.getTestMethod());
try {
String result = (String) js.executeAsyncScript(
readResource("teavm-selenium.js"),
readFile(new File(directory, runtimeScript)),
readFile(new File(directory, testCase.getTestScript())),
readResource("teavm-selenium-adapter.js"));
String result = strategy.runTest(log, runtimeScript, testCase);
if (result == null) {
log.info("Test failed: " + testCase.getTestMethod());
localReport.get().add(TestResult.error(ref, null, null));
}
ObjectMapper mapper = new ObjectMapper();
ObjectNode resultObject = (ObjectNode) mapper.readTree(result);
String status = resultObject.get("status").asText();
MethodReference ref = MethodReference.parse(testCase.getTestMethod());
switch (status) {
case "ok":
if (testCase.getExpectedExceptions().isEmpty()) {
@ -181,28 +153,6 @@ public class SeleniumTestRunner {
}
} catch (IOException e) {
log.error(e);
} catch (WebDriverException e) {
log.error("Error occured running test " + testCase.getTestMethod(), e);
@SuppressWarnings("unchecked")
List<Object> errors = (List<Object>) js.executeScript("return window.jsErrors;");
for (Object error : errors) {
log.error(" -- additional error: " + error);
}
}
}
private String readFile(File file) throws IOException {
try (InputStream input = new FileInputStream(file)) {
return IOUtils.toString(input, "UTF-8");
}
}
private String readResource(String resourceName) throws IOException {
try (InputStream input = BuildJavascriptTestMojo.class.getClassLoader().getResourceAsStream(resourceName)) {
if (input == null) {
return "";
}
return IOUtils.toString(input, "UTF-8");
}
}

View File

@ -0,0 +1,65 @@
function(callback) {
var JUnitClient = {}
JUnitClient.run = function() {
$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:
instance = new TestClass();
ptr = 1;
case 1:
try {
initInstance(instance);
} catch (e) {
message = {};
JUnitClient.makeErrorMessage(message, e);
break loop;
}
if (thread.isSuspending()) {
thread.push(instance);
thread.push(ptr);
return;
}
ptr = 2;
case 2:
try {
runTest(instance);
} catch (e) {
message = {};
JUnitClient.makeErrorMessage(message, e);
break loop;
}
if (thread.isSuspending()) {
thread.push(instance);
thread.push(ptr);
return;
}
message = {};
message.status = "ok";
break loop;
}}
callback.complete(JSON.stringify(message));
})
}
JUnitClient.makeErrorMessage = function(message, e) {
message.status = "exception";
var stack = e.stack;
if (e.$javaException && e.$javaException.constructor.$meta) {
message.exception = e.$javaException.constructor.$meta.name;
message.stack = e.$javaException.constructor.$meta.name + ": ";
var exceptionMessage = extractException(e.$javaException);
message.stack += exceptionMessage ? $rt_ustr(exceptionMessage) : "";
}
message.stack += "\n" + stack;
}
window.JUnitClient = JUnitClient;
}