Add JS test runner that runs tests right in the browser

This commit is contained in:
Alexey Andreev 2021-03-07 15:56:48 +03:00
parent 8b4f401bcb
commit 61db54e848
22 changed files with 1089 additions and 48 deletions

View File

@ -73,7 +73,7 @@ function launchTest(argument, callback) {
function buildErrorMessage(e) { function buildErrorMessage(e) {
let stack = ""; let stack = "";
var je = main.javaException(e); let je = main.javaException(e);
if (je && je.constructor.$meta) { if (je && je.constructor.$meta) {
stack = je.constructor.$meta.name + ": "; stack = je.constructor.$meta.name + ": ";
stack += je.getMessage(); stack += je.getMessage();
@ -85,8 +85,8 @@ function launchTest(argument, callback) {
} }
function launchWasmTest(path, argument, callback) { function launchWasmTest(path, argument, callback) {
var output = []; let output = [];
var outputBuffer = ""; let outputBuffer = "";
function putwchar(charCode) { function putwchar(charCode) {
if (charCode === 10) { if (charCode === 10) {

View File

@ -44,6 +44,12 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Alexey Andreev. * Copyright 2021 konsoletyper.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.teavm.devserver.deobfuscate; package org.teavm.tooling.deobfuscate.js;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Alexey Andreev. * Copyright 2021 konsoletyper.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.teavm.devserver.deobfuscate; package org.teavm.tooling.deobfuscate.js;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -26,6 +26,7 @@ import org.teavm.debugging.information.SourceLocation;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.ajax.XMLHttpRequest; import org.teavm.jso.ajax.XMLHttpRequest;
import org.teavm.jso.core.JSArray; import org.teavm.jso.core.JSArray;
import org.teavm.jso.core.JSObjects;
import org.teavm.jso.core.JSRegExp; import org.teavm.jso.core.JSRegExp;
import org.teavm.jso.core.JSString; import org.teavm.jso.core.JSString;
import org.teavm.jso.typedarrays.ArrayBuffer; import org.teavm.jso.typedarrays.ArrayBuffer;
@ -33,9 +34,16 @@ import org.teavm.jso.typedarrays.Int8Array;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
public final class Deobfuscator { public final class Deobfuscator {
private static final JSRegExp FRAME_PATTERN = JSRegExp.create("^ +at ([^(]+) *\\((.+):([0-9]+):([0-9]+)\\) *$"); private static final JSRegExp FRAME_PATTERN = JSRegExp.create(""
+ "(^ +at ([^(]+) *\\((.+):([0-9]+):([0-9]+)\\) *$)|"
+ "(^([^@]*)@(.+):([0-9]+):([0-9]+)$)");
private DebugInformation debugInformation;
private String classesFileName;
private Deobfuscator() { public Deobfuscator(ArrayBuffer buffer, String classesFileName) throws IOException {
Int8Array array = Int8Array.create(buffer);
debugInformation = DebugInformation.read(new Int8ArrayInputStream(array));
this.classesFileName = classesFileName;
} }
public static void main(String[] args) { public static void main(String[] args) {
@ -52,17 +60,7 @@ public final class Deobfuscator {
xhr.send(); xhr.send();
} }
private static void installDeobfuscator(ArrayBuffer buffer, String classesFileName) { public Frame[] deobfuscate(String stack) {
Int8Array array = Int8Array.create(buffer);
DebugInformation debugInformation;
try {
debugInformation = DebugInformation.read(new Int8ArrayInputStream(array));
} catch (IOException e) {
e.printStackTrace();
return;
}
setDeobfuscateFunction(stack -> {
List<Frame> frames = new ArrayList<>(); List<Frame> frames = new ArrayList<>();
for (String line : splitLines(stack)) { for (String line : splitLines(stack)) {
JSArray<JSString> groups = FRAME_PATTERN.exec(JSString.valueOf(line)); JSArray<JSString> groups = FRAME_PATTERN.exec(JSString.valueOf(line));
@ -70,10 +68,15 @@ public final class Deobfuscator {
continue; continue;
} }
String functionName = groups.get(1).stringValue(); int groupOffset = 1;
String fileName = groups.get(2).stringValue(); if (JSObjects.isUndefined(groups.get(1))) {
int lineNumber = Integer.parseInt(groups.get(3).stringValue()); groupOffset = 6;
int columnNumber = Integer.parseInt(groups.get(4).stringValue()); }
String functionName = groups.get(1 + groupOffset).stringValue();
String fileName = groups.get(2 + groupOffset).stringValue();
int lineNumber = Integer.parseInt(groups.get(3 + groupOffset).stringValue());
int columnNumber = Integer.parseInt(groups.get(4 + groupOffset).stringValue());
List<Frame> framesPerLine = deobfuscateFrames(debugInformation, classesFileName, fileName, List<Frame> framesPerLine = deobfuscateFrames(debugInformation, classesFileName, fileName,
lineNumber, columnNumber); lineNumber, columnNumber);
if (framesPerLine == null) { if (framesPerLine == null) {
@ -82,7 +85,18 @@ public final class Deobfuscator {
frames.addAll(framesPerLine); frames.addAll(framesPerLine);
} }
return frames.toArray(new Frame[0]); return frames.toArray(new Frame[0]);
}); }
private static void installDeobfuscator(ArrayBuffer buffer, String classesFileName) {
Deobfuscator deobfuscator;
try {
deobfuscator = new Deobfuscator(buffer, classesFileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
setDeobfuscateFunction(deobfuscator::deobfuscate);
DeobfuscatorCallback callback = getCallback(); DeobfuscatorCallback callback = getCallback();
if (callback != null) { if (callback != null) {
callback.run(); callback.run();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Alexey Andreev. * Copyright 2021 konsoletyper.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.teavm.devserver.deobfuscate; package org.teavm.tooling.deobfuscate.js;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;

View File

@ -0,0 +1,24 @@
/*
* Copyright 2021 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.tooling.deobfuscate.js;
import org.teavm.jso.JSObject;
import org.teavm.jso.typedarrays.ArrayBuffer;
public interface DeobfuscatorJs extends JSObject {
DeobfuscateFunction create(ArrayBuffer buffer, String classesFileName);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2021 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.tooling.deobfuscate.js;
import java.io.IOException;
import org.teavm.jso.JSBody;
import org.teavm.jso.typedarrays.ArrayBuffer;
public final class DeobfuscatorLib implements DeobfuscatorJs {
private DeobfuscatorLib() {
}
@Override
public DeobfuscateFunction create(ArrayBuffer buffer, String classesFileName) {
try {
return new Deobfuscator(buffer, classesFileName)::deobfuscate;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
install(new DeobfuscatorLib());
}
@JSBody(params = "instance", script =
"deobfuscator.create = function(buffer, classesFileName) {"
+ "return instance.create(buffer, classesFileName);"
+ "}"
)
private static native void install(DeobfuscatorJs js);
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Alexey Andreev. * Copyright 2021 konsoletyper.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.teavm.devserver.deobfuscate; package org.teavm.tooling.deobfuscate.js;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Alexey Andreev. * Copyright 2021 Alexey Andreev.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.teavm.devserver.deobfuscate; package org.teavm.tooling.deobfuscate.js;
import java.io.InputStream; import java.io.InputStream;
import org.teavm.jso.typedarrays.Int8Array; import org.teavm.jso.typedarrays.Int8Array;

View File

@ -61,12 +61,6 @@
<artifactId>teavm-core</artifactId> <artifactId>teavm-core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.teavm</groupId> <groupId>org.teavm</groupId>
<artifactId>teavm-tooling</artifactId> <artifactId>teavm-tooling</artifactId>
@ -134,7 +128,7 @@
<targetFileName>deobfuscator.js</targetFileName> <targetFileName>deobfuscator.js</targetFileName>
<minifying>true</minifying> <minifying>true</minifying>
<optimizationLevel>ADVANCED</optimizationLevel> <optimizationLevel>ADVANCED</optimizationLevel>
<mainClass>org.teavm.devserver.deobfuscate.Deobfuscator</mainClass> <mainClass>org.teavm.tooling.deobfuscate.js.Deobfuscator</mainClass>
<entryPointName>$teavm_deobfuscator</entryPointName> <entryPointName>$teavm_deobfuscator</entryPointName>
</configuration> </configuration>
</execution> </execution>

View File

@ -50,6 +50,34 @@
<artifactId>htmlunit</artifactId> <artifactId>htmlunit</artifactId>
<version>2.33</version> <version>2.33</version>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -70,6 +98,41 @@
<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.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>${project.version}</version>
<dependencies>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-classlib</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>compile-deobfuscator</id>
<goals>
<goal>compile</goal>
</goals>
<phase>process-classes</phase>
<configuration>
<targetDirectory>${project.build.directory}/classes/test-server</targetDirectory>
<targetFileName>deobfuscator.js</targetFileName>
<minifying>true</minifying>
<optimizationLevel>ADVANCED</optimizationLevel>
<mainClass>org.teavm.tooling.deobfuscate.js.DeobfuscatorLib</mainClass>
<entryPointName>deobfuscator</entryPointName>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@ -0,0 +1,346 @@
/*
* Copyright 2021 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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
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.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
public class BrowserRunStrategy implements TestRunStrategy {
private boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true"));
private final File baseDir;
private final String type;
private final Function<String, Process> browserRunner;
private Process browserProcess;
private Server server;
private int port;
private AtomicInteger idGenerator = new AtomicInteger(0);
private AtomicReference<Session> wsSession = new AtomicReference<>();
private CountDownLatch wsSessionReady = new CountDownLatch(1);
private ConcurrentMap<Integer, TestRun> awaitingRuns = new ConcurrentHashMap<>();
private ObjectMapper objectMapper = new ObjectMapper();
public BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
this.baseDir = baseDir;
this.type = type;
this.browserRunner = browserRunner;
}
@Override
public void beforeAll() {
runServer();
browserProcess = browserRunner.apply("http://localhost:" + port + "/index.html");
}
private void runServer() {
server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
TestCodeServlet servlet = new TestCodeServlet();
ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.setAsyncSupported(true);
context.addServlet(servletHolder, "/*");
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
port = connector.getLocalPort();
}
@Override
public void afterAll() {
try {
server.stop();
} catch (Exception e) {
e.printStackTrace();
}
if (browserProcess != null) {
browserProcess.destroy();
}
}
@Override
public void beforeThread() {
}
@Override
public void afterThread() {
}
@Override
public void runTest(TestRun run) throws IOException {
try {
while (!wsSessionReady.await(1L, TimeUnit.SECONDS)) {
// keep waiting
}
} catch (InterruptedException e) {
run.getCallback().error(e);
return;
}
Session ws = wsSession.get();
if (ws == null) {
return;
}
int id = idGenerator.incrementAndGet();
awaitingRuns.put(id, run);
JsonNodeFactory nf = objectMapper.getNodeFactory();
ObjectNode node = nf.objectNode();
node.set("id", nf.numberNode(id));
ArrayNode array = nf.arrayNode();
node.set("tests", array);
File file = new File(run.getBaseDirectory(), run.getFileName()).getAbsoluteFile();
String relPath = baseDir.getAbsoluteFile().toPath().relativize(file.toPath()).toString();
ObjectNode testNode = nf.objectNode();
testNode.set("type", nf.textNode(type));
testNode.set("name", nf.textNode(run.getFileName()));
testNode.set("file", nf.textNode("tests/" + relPath));
if (run.getArgument() != null) {
testNode.set("argument", nf.textNode(run.getArgument()));
}
array.add(testNode);
String message = node.toString();
ws.getRemote().sendStringByFuture(message);
}
class TestCodeServlet extends HttpServlet {
private WebSocketServletFactory wsFactory;
private Map<String, String> contentCache = new ConcurrentHashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
WebSocketPolicy wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER);
wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy);
wsFactory.setCreator((req, resp) -> new TestCodeSocket());
try {
wsFactory.start();
} catch (Exception e) {
throw new ServletException(e);
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getRequestURI();
if (path != null) {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (req.getMethod().equals("GET")) {
switch (path) {
case "/index.html":
case "/frame.html": {
String content = getFromCache(path, "true".equals(req.getParameter("logging")));
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/html");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
}
case "/client.js":
case "/frame.js":
case "/deobfuscator.js": {
String content = getFromCache(path, false);
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("application/javascript");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
}
}
if (path.startsWith("/tests/")) {
String relPath = path.substring("/tests/".length());
File file = new File(baseDir, relPath);
if (file.isFile()) {
resp.setStatus(HttpServletResponse.SC_OK);
if (file.getName().endsWith(".js")) {
resp.setContentType("application/javascript");
} else if (file.getName().endsWith(".wasm")) {
resp.setContentType("application/wasm");
}
try (FileInputStream input = new FileInputStream(file)) {
copy(input, resp.getOutputStream());
}
resp.getOutputStream().flush();
}
}
}
if (path.equals("/ws") && wsFactory.isUpgradeRequest(req, resp)
&& (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted())) {
return;
}
}
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
private String getFromCache(String fileName, boolean logging) {
return contentCache.computeIfAbsent(fileName, fn -> {
ClassLoader loader = BrowserRunStrategy.class.getClassLoader();
try (InputStream input = loader.getResourceAsStream("test-server" + fn);
Reader reader = new InputStreamReader(input)) {
StringBuilder sb = new StringBuilder();
char[] buffer = new char[2048];
while (true) {
int charsRead = reader.read(buffer);
if (charsRead < 0) {
break;
}
sb.append(buffer, 0, charsRead);
}
return sb.toString()
.replace("{{PORT}}", String.valueOf(port))
.replace("\"{{LOGGING}}\"", String.valueOf(logging))
.replace("\"{{DEOBFUSCATION}}\"", String.valueOf(decodeStack));
} catch (IOException e) {
e.printStackTrace();
return null;
}
});
}
private void copy(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[2048];
while (true) {
int bytes = input.read(buffer);
if (bytes < 0) {
break;
}
output.write(buffer, 0, bytes);
}
}
}
class TestCodeSocket extends WebSocketAdapter {
private AtomicBoolean ready = new AtomicBoolean(false);
@Override
public void onWebSocketConnect(Session sess) {
if (wsSession.compareAndSet(null, sess)) {
ready.set(true);
wsSessionReady.countDown();
} else {
System.err.println("Link opened in multiple browsers");
}
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
if (ready.get()) {
System.err.println("Browser has disconnected");
for (TestRun run : awaitingRuns.values()) {
run.getCallback().error(new RuntimeException("Browser disconnected unexpectedly"));
}
}
}
@Override
public void onWebSocketText(String message) {
if (!ready.get()) {
return;
}
JsonNode node;
try {
node = objectMapper.readTree(new StringReader(message));
} catch (IOException e) {
throw new RuntimeException(e);
}
int id = node.get("id").asInt();
TestRun run = awaitingRuns.remove(id);
if (run == null) {
System.err.println("Unexpected run id: " + id);
return;
}
JsonNode resultNode = node.get("result");
JsonNode log = resultNode.get("log");
if (log != null) {
for (JsonNode logEntry : log) {
String str = logEntry.get("message").asText();
switch (logEntry.get("type").asText()) {
case "stdout":
System.out.println(str);
break;
case "stderr":
System.err.println(str);
break;
}
}
}
String status = resultNode.get("status").asText();
if (status.equals("OK")) {
run.getCallback().complete();
} else {
run.getCallback().error(new RuntimeException(resultNode.get("errorMessage").asText()));
}
}
}
}

View File

@ -33,6 +33,14 @@ class CRunStrategy implements TestRunStrategy {
this.compilerCommand = compilerCommand; this.compilerCommand = compilerCommand;
} }
@Override
public void beforeAll() {
}
@Override
public void afterAll() {
}
@Override @Override
public void beforeThread() { public void beforeThread() {
} }

View File

@ -41,6 +41,14 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
private ThreadLocal<HtmlPage> page = new ThreadLocal<>(); private ThreadLocal<HtmlPage> page = new ThreadLocal<>();
private int runs; private int runs;
@Override
public void beforeAll() {
}
@Override
public void afterAll() {
}
@Override @Override
public void beforeThread() { public void beforeThread() {
init(); init();

View File

@ -17,10 +17,12 @@ package org.teavm.junit;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
@ -164,7 +166,12 @@ public class TeaVMTestRunner extends Runner implements Filterable {
case "htmlunit": case "htmlunit":
jsRunStrategy = new HtmlUnitRunStrategy(); jsRunStrategy = new HtmlUnitRunStrategy();
break; break;
case "": case "browser":
jsRunStrategy = new BrowserRunStrategy(outputDir, "JAVASCRIPT", this::customBrowser);
break;
case "browser-chrome":
jsRunStrategy = new BrowserRunStrategy(outputDir, "JAVASCRIPT", this::chromeBrowser);
break;
case "none": case "none":
jsRunStrategy = null; jsRunStrategy = null;
break; break;
@ -180,6 +187,73 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
private Process customBrowser(String url) {
System.out.println("Open link to run tests: " + url + "?logging=true");
return null;
}
private Process chromeBrowser(String url) {
File temp;
try {
temp = File.createTempFile("teavm", "teavm");
temp.delete();
temp.mkdirs();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
deleteDir(temp);
}));
System.out.println("Running chrome with user data dir: " + temp.getAbsolutePath());
ProcessBuilder pb = new ProcessBuilder(
"google-chrome-stable",
"--headless",
"--disable-gpu",
"--remote-debugging-port=9222",
"--no-first-run",
"--user-data-dir=" + temp.getAbsolutePath(),
url
);
Process process = pb.start();
logStream(process.getInputStream(), "Chrome stdout");
logStream(process.getErrorStream(), "Chrome stderr");
new Thread(() -> {
try {
System.out.println("Chrome process terminated with code: " + process.waitFor());
} catch (InterruptedException e) {
// ignore
}
});
return process;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void logStream(InputStream stream, String name) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
System.out.println(name + ": " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void deleteDir(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
deleteDir(file);
} else {
file.delete();
}
}
dir.delete();
}
@Override @Override
public Description getDescription() { public Description getDescription() {
if (suiteDescription == null) { if (suiteDescription == null) {
@ -707,7 +781,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
}; };
} }
return compile(configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js", return compile(configuration, targetSupplier, TestJsEntryPoint.class.getName(), path, ".js",
postBuild, false, additionalProcessing, baseName); postBuild, false, additionalProcessing, baseName);
} }

View File

@ -0,0 +1,57 @@
/*
* Copyright 2021 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.jso.JSBody;
final class TestJsEntryPoint {
private TestJsEntryPoint() {
}
public static void main(String[] args) throws Throwable {
try {
TestEntryPoint.run(args.length > 0 ? args[0] : null);
} catch (Throwable e) {
StringBuilder sb = new StringBuilder();
printStackTrace(e, sb);
saveJavaException(sb.toString());
throw e;
}
}
private static void printStackTrace(Throwable e, StringBuilder stream) {
stream.append(e.getClass().getName());
String message = e.getLocalizedMessage();
if (message != null) {
stream.append(": " + message);
}
stream.append("\n");
StackTraceElement[] stackTrace = e.getStackTrace();
if (stackTrace != null) {
for (StackTraceElement element : stackTrace) {
stream.append("\tat ");
stream.append(element).append("\n");
}
}
if (e.getCause() != null && e.getCause() != e) {
stream.append("Caused by: ");
printStackTrace(e.getCause(), stream);
}
}
@JSBody(params = "e", script = "window.teavmException = e")
private static native void saveJavaException(String e);
}

View File

@ -18,6 +18,10 @@ package org.teavm.junit;
import java.io.IOException; import java.io.IOException;
interface TestRunStrategy { interface TestRunStrategy {
void beforeAll();
void afterAll();
void beforeThread(); void beforeThread();
void afterThread(); void afterThread();

View File

@ -37,6 +37,11 @@ class TestRunner {
public void init() { public void init() {
latch = new CountDownLatch(numThreads); latch = new CountDownLatch(numThreads);
strategy.beforeAll();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
strategy.afterAll();
}));
for (int i = 0; i < numThreads; ++i) { for (int i = 0; i < numThreads; ++i) {
Thread thread = new Thread(() -> { Thread thread = new Thread(() -> {
strategy.beforeThread(); strategy.beforeThread();

View File

@ -0,0 +1,146 @@
/*
* Copyright 2021 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.
*/
"use strict";
let logging = false;
let deobfuscation = false;
deobfuscator();
function tryConnect() {
let ws = new WebSocket("ws://localhost:{{PORT}}/ws");
ws.onopen = () => {
if (logging) {
console.log("Connection established");
}
listen(ws);
};
ws.onclose = () => {
ws.close();
setTimeout(() => {
tryConnect();
}, 500);
};
ws.onerror = err => {
if (logging) {
console.log("Could not connect WebSocket", err);
}
}
}
function listen(ws) {
ws.onmessage = (event) => {
let request = JSON.parse(event.data);
if (logging) {
console.log("Request #" + request.id + " received");
}
runTests(ws, request.id, request.tests, 0);
}
}
function runTests(ws, suiteId, tests, index) {
if (index === tests.length) {
return;
}
let test = tests[index];
runSingleTest(test, result => {
if (logging) {
console.log("Sending response #" + suiteId);
}
ws.send(JSON.stringify({
id: suiteId,
index: index,
result: result
}));
runTests(ws, suiteId, tests, index + 1);
});
}
let lastDeobfuscator = null;
let lastDeobfuscatorFile = null;
let lastDeobfuscatorPromise = null;
function runSingleTest(test, callback) {
if (logging) {
console.log("Running test " + test.name);
}
if (deobfuscation) {
const fileName = test.file + ".teavmdbg";
if (lastDeobfuscatorFile === fileName) {
if (lastDeobfuscatorPromise === null) {
runSingleTestWithDeobfuscator(test, lastDeobfuscator, callback);
} else {
lastDeobfuscatorPromise.then(value => {
runSingleTestWithDeobfuscator(test, value, callback);
})
}
} else {
lastDeobfuscatorFile = fileName;
lastDeobfuscator = null;
const xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
lastDeobfuscatorPromise = new Promise(resolve => {
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
const newDeobfuscator = xhr.status === 200
? deobfuscator.create(xhr.response, "http://localhost:{{PORT}}/" + test.file)
: null;
if (lastDeobfuscatorFile === fileName) {
lastDeobfuscator = newDeobfuscator;
lastDeobfuscatorPromise = null;
}
resolve(newDeobfuscator);
runSingleTestWithDeobfuscator(test, newDeobfuscator, callback);
}
}
xhr.open("GET", fileName);
xhr.send();
});
}
} else {
runSingleTestWithDeobfuscator(test, null, callback);
}
}
function runSingleTestWithDeobfuscator(test, deobfuscator, callback) {
let iframe = document.createElement("iframe");
document.body.appendChild(iframe);
let handshakeListener = handshakeEvent => {
if (handshakeEvent.source !== iframe.contentWindow || handshakeEvent.data !== "ready") {
return;
}
window.removeEventListener("message", handshakeListener);
let listener = event => {
if (event.source !== iframe.contentWindow) {
return;
}
window.removeEventListener("message", listener);
document.body.removeChild(iframe);
callback(event.data);
};
window.addEventListener("message", listener);
iframe.contentWindow.$rt_decodeStack = deobfuscator;
iframe.contentWindow.postMessage(test, "*");
};
window.addEventListener("message", handshakeListener);
iframe.src = "about:blank";
iframe.src = "frame.html";
}

View File

@ -0,0 +1,25 @@
<!--
~ Copyright 2021 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="frame.js"></script>
</head>
<body onload="start()">
</body>
</html>

View File

@ -0,0 +1,190 @@
/*
* Copyright 2021 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.
*/
"use strict";
window.addEventListener("message", event => {
let request = event.data;
switch (request.type) {
case "JAVASCRIPT":
appendFiles([request.file], 0, () => {
launchTest(request.argument, response => {
event.source.postMessage(response, "*");
});
}, error => {
event.source.postMessage(wrapResponse({ status: "failed", errorMessage: error }), "*");
});
break;
case "WASM":
const runtimeFile = request.file + "-runtime.js";
appendFiles([runtimeFile], 0, () => {
launchWasmTest(request.file, equest.argument, response => {
event.source.postMessage(response, "*");
});
}, error => {
event.source.postMessage(wrapResponse({ status: "failed", errorMessage: error }), "*");
});
break;
}
});
function appendFiles(files, index, callback, errorCallback) {
if (index === files.length) {
callback();
} else {
let fileName = files[index];
let script = document.createElement("script");
script.onload = () => {
appendFiles(files, index + 1, callback, errorCallback);
};
script.onerror = () => {
errorCallback("failed to load script " + fileName);
};
script.src = fileName;
document.body.appendChild(script);
}
}
function launchTest(argument, callback) {
main(argument ? [argument] : [], result => {
if (result instanceof Error) {
callback(wrapResponse({
status: "failed",
errorMessage: buildErrorMessage(result)
}));
} else {
callback({ status: "OK" });
}
});
function buildErrorMessage(e) {
if (typeof $rt_decodeStack === "function" && typeof teavmException == "string") {
return teavmException;
}
let stack = "";
let je = main.javaException(e);
if (je && je.constructor.$meta) {
stack = je.constructor.$meta.name + ": ";
stack += je.getMessage();
stack += "\n";
}
stack += e.stack;
return stack;
}
}
function launchWasmTest(path, argument, callback) {
let output = [];
let outputBuffer = "";
function putwchar(charCode) {
if (charCode === 10) {
switch (outputBuffer) {
case "SUCCESS":
callback(wrapResponse({ status: "OK" }));
break;
case "FAILURE":
callback(wrapResponse({
status: "failed",
errorMessage: output.join("\n")
}));
break;
default:
output.push(outputBuffer);
outputBuffer = "";
}
} else {
outputBuffer += String.fromCharCode(charCode);
}
}
TeaVM.wasm.run(path, {
installImports: function(o) {
o.teavm.putwchar = putwchar;
},
errorCallback: function(err) {
callback(wrapResponse({
status: "failed",
errorMessage: err.message + '\n' + err.stack
}));
}
});
}
function start() {
window.parent.postMessage("ready", "*");
}
let log = [];
function wrapResponse(response) {
if (log.length > 0) {
response.log = log;
log = [];
}
return response;
}
let $rt_putStdoutCustom = createOutputFunction(msg => {
log.push({ type: "stdout", message: msg });
});
let $rt_putStderrCustom = createOutputFunction(msg => {
log.push({ type: "stderr", message: msg });
});
function createOutputFunction(printFunction) {
let buffer = "";
let utf8Buffer = 0;
let utf8Remaining = 0;
function putCodePoint(ch) {
if (ch === 0xA) {
printFunction(buffer);
buffer = "";
} else if (ch < 0x10000) {
buffer += String.fromCharCode(ch);
} else {
ch = (ch - 0x10000) | 0;
var hi = (ch >> 10) + 0xD800;
var lo = (ch & 0x3FF) + 0xDC00;
buffer += String.fromCharCode(hi, lo);
}
}
return ch => {
if ((ch & 0x80) === 0) {
putCodePoint(ch);
} else if ((ch & 0xC0) === 0x80) {
if (utf8Buffer > 0) {
utf8Remaining <<= 6;
utf8Remaining |= ch & 0x3F;
if (--utf8Buffer === 0) {
putCodePoint(utf8Remaining);
}
}
} else if ((ch & 0xE0) === 0xC0) {
utf8Remaining = ch & 0x1F;
utf8Buffer = 1;
} else if ((ch & 0xF0) === 0xE0) {
utf8Remaining = ch & 0x0F;
utf8Buffer = 2;
} else if ((ch & 0xF8) === 0xF0) {
utf8Remaining = ch & 0x07;
utf8Buffer = 3;
}
};
}

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2021 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="deobfuscator.js"></script>
<script src="client.js"></script>
</head>
<body>
<script>
logging = "{{LOGGING}}";
deobfuscation = "{{DEOBFUSCATION}}";
tryConnect();
</script>
</body>
</html>