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 - $HOME/.m2
after_script: after_script:
- rm -rf $HOME/.m2/repository/org/teavm - 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.incremental>false</teavm.test.incremental>
<teavm.test.threads>1</teavm.test.threads> <teavm.test.threads>1</teavm.test.threads>
<teavm.test.selenium></teavm.test.selenium> <teavm.test.selenium></teavm.test.selenium>
<teavm.test.skip>true</teavm.test.skip>
</properties> </properties>
<dependencies> <dependencies>
@ -100,6 +101,7 @@
<goal>test</goal> <goal>test</goal>
</goals> </goals>
<configuration> <configuration>
<skip>${teavm.test.skip}</skip>
<numThreads>${teavm.test.threads}</numThreads> <numThreads>${teavm.test.threads}</numThreads>
<seleniumURL>${teavm.test.selenium}</seleniumURL> <seleniumURL>${teavm.test.selenium}</seleniumURL>
</configuration> </configuration>

View File

@ -69,6 +69,11 @@
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.18</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
@ -103,6 +108,14 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
</plugin> </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> </plugins>
</build> </build>
</project> </project>

View File

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

View File

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

View File

@ -53,28 +53,25 @@ public class RunTestsMojo extends AbstractMojo {
@Parameter @Parameter
private int numThreads = 1; private int numThreads = 1;
@Parameter
private boolean skip;
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException { public void execute() throws MojoExecutionException, MojoFailureException {
if (seleniumURL == null || seleniumURL.isEmpty()) { if (skip) {
getLog().info("Tests build skipped as selenium URL was not specified"); getLog().info("Tests run skipped as specified by skip property");
return; return;
} }
if (System.getProperty("maven.test.skip", "false").equals("true") || if (System.getProperty("maven.test.skip", "false").equals("true")
System.getProperty("skipTests") != null) { || System.getProperty("skipTests") != null) {
getLog().info("Tests build skipped as specified by system property"); getLog().info("Tests run skipped as specified by system property");
return; return;
} }
SeleniumTestRunner runner = new SeleniumTestRunner(); TestRunner runner = new TestRunner(pickStrategy());
runner.setLog(getLog()); runner.setLog(getLog());
runner.setDirectory(testDirectory);
runner.setNumThreads(numThreads); runner.setNumThreads(numThreads);
try {
runner.setUrl(new URL(seleniumURL));
} catch (MalformedURLException e) {
throw new MojoFailureException("Can't parse URL: " + seleniumURL, e);
}
TestPlan plan; TestPlan plan;
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
@ -89,6 +86,18 @@ public class RunTestsMojo extends AbstractMojo {
processReport(runner.getReport()); 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 { private void processReport(TestReport report) throws MojoExecutionException, MojoFailureException {
if (report.getResults().isEmpty()) { if (report.getResults().isEmpty()) {
getLog().info("No tests ran"); 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; package org.teavm.maven;
import java.util.ArrayList; 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.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -29,13 +25,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
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.apache.commons.io.IOUtils;
import org.apache.maven.plugin.logging.Log; 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.model.MethodReference;
import org.teavm.tooling.testing.TestCase; import org.teavm.tooling.testing.TestCase;
import org.teavm.tooling.testing.TestGroup; import org.teavm.tooling.testing.TestGroup;
@ -45,38 +35,24 @@ import org.teavm.tooling.testing.TestPlan;
* *
* @author Alexey Andreev * @author Alexey Andreev
*/ */
public class SeleniumTestRunner { public class TestRunner {
private URL url;
private int numThreads = 1; private int numThreads = 1;
private ThreadLocal<WebDriver> webDriver = new ThreadLocal<>(); private TestRunStrategy strategy;
private BlockingQueue<Runnable> seleniumTaskQueue = new LinkedBlockingQueue<>(); private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
private CountDownLatch latch; private CountDownLatch latch;
private volatile boolean seleniumStopped = false; private volatile boolean stopped;
private Log log; private Log log;
private List<TestResult> report = new CopyOnWriteArrayList<>(); private List<TestResult> report = new CopyOnWriteArrayList<>();
private ThreadLocal<List<TestResult>> localReport = new ThreadLocal<>(); private ThreadLocal<List<TestResult>> localReport = new ThreadLocal<>();
private File directory = new File(".");
public URL getUrl() { public TestRunner(TestRunStrategy strategy) {
return url; this.strategy = strategy;
}
public void setUrl(URL url) {
this.url = url;
} }
public void setLog(Log log) { public void setLog(Log log) {
this.log = log; this.log = log;
} }
public File getDirectory() {
return directory;
}
public void setDirectory(File directory) {
this.directory = directory;
}
public int getNumThreads() { public int getNumThreads() {
return numThreads; return numThreads;
} }
@ -100,13 +76,12 @@ public class SeleniumTestRunner {
latch = new CountDownLatch(numThreads); latch = new CountDownLatch(numThreads);
for (int i = 0; i < numThreads; ++i) { for (int i = 0; i < numThreads; ++i) {
new Thread(() -> { new Thread(() -> {
RemoteWebDriver driver = new RemoteWebDriver(DesiredCapabilities.chrome()); strategy.beforeThread();
webDriver.set(driver);
localReport.set(new ArrayList<>()); localReport.set(new ArrayList<>());
while (!seleniumStopped || !seleniumTaskQueue.isEmpty()) { while (!stopped || !taskQueue.isEmpty()) {
Runnable task; Runnable task;
try { try {
task = seleniumTaskQueue.poll(100, TimeUnit.MILLISECONDS); task = taskQueue.poll(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }
@ -116,19 +91,18 @@ public class SeleniumTestRunner {
} }
report.addAll(localReport.get()); report.addAll(localReport.get());
localReport.remove(); localReport.remove();
webDriver.get().close(); strategy.afterThread();
webDriver.remove();
latch.countDown(); latch.countDown();
}).start(); }).start();
} }
} }
private void addSeleniumTask(Runnable runnable) { private void addTask(Runnable runnable) {
seleniumTaskQueue.add(runnable); taskQueue.add(runnable);
} }
private void stopSelenium() { private void stopSelenium() {
seleniumStopped = true; stopped = true;
} }
private void waitForCompletion() { private void waitForCompletion() {
@ -140,22 +114,20 @@ public class SeleniumTestRunner {
} }
private void run(String runtimeScript, TestCase testCase) { private void run(String runtimeScript, TestCase testCase) {
addSeleniumTask(() -> runImpl(runtimeScript, testCase)); addTask(() -> runImpl(runtimeScript, testCase));
} }
private void runImpl(String runtimeScript, TestCase testCase) { private void runImpl(String runtimeScript, TestCase testCase) {
webDriver.get().manage().timeouts().setScriptTimeout(2, TimeUnit.SECONDS); MethodReference ref = MethodReference.parse(testCase.getTestMethod());
JavascriptExecutor js = (JavascriptExecutor) webDriver.get();
try { try {
String result = (String) js.executeAsyncScript( String result = strategy.runTest(log, runtimeScript, testCase);
readResource("teavm-selenium.js"), if (result == null) {
readFile(new File(directory, runtimeScript)), log.info("Test failed: " + testCase.getTestMethod());
readFile(new File(directory, testCase.getTestScript())), localReport.get().add(TestResult.error(ref, null, null));
readResource("teavm-selenium-adapter.js")); }
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
ObjectNode resultObject = (ObjectNode) mapper.readTree(result); ObjectNode resultObject = (ObjectNode) mapper.readTree(result);
String status = resultObject.get("status").asText(); String status = resultObject.get("status").asText();
MethodReference ref = MethodReference.parse(testCase.getTestMethod());
switch (status) { switch (status) {
case "ok": case "ok":
if (testCase.getExpectedExceptions().isEmpty()) { if (testCase.getExpectedExceptions().isEmpty()) {
@ -181,28 +153,6 @@ public class SeleniumTestRunner {
} }
} catch (IOException e) { } catch (IOException e) {
log.error(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;
}